tag:blogger.com,1999:blog-212226252024-03-07T04:21:05.472-05:00The Pothoven PostInformation, experience and learning of Steven Pothoven -- usually technology related.Unknownnoreply@blogger.comBlogger104125tag:blogger.com,1999:blog-21222625.post-29695196555318315042023-07-07T13:55:00.003-04:002023-07-11T08:32:37.744-04:00Migrating away from Angular flex-layout<p>In a <a href="https://blog.angular.io/modern-css-in-angular-layouts-4a259dca9127" rel="nofollow" target="_blank">blog post from October 22, 2022</a> the Angular team announced</p>
<blockquote>Layout has made significant advancements since Angular’s early days. Based on advancements to native layout solutions and removal of support for IE11, the Angular team will stop publishing new releases of the experimental @angular/flex-layout library starting in v15.</blockquote>
<p>They then proceeded to layout migration alternatives.</p>
<div>
<ul style="text-align: left;">
<li>CSS Flexbox</li>
<li>CSS Grid</li>
<li>TailwindCSS</li>
</ul>
</div>
<p>This has resulted in considerable consternation for developers. I do not wish to restate what others have already said about the problem and will instead simply point you to a good article on the topic entitled, <a href="https://betterprogramming.pub/farewell-flex-layouts-is-angular-starting-to-become-a-worry-4eea953b717b" rel="nofollow" target="_blank">Why the Deprecation of Flex-Layouts Is Concerning for Angular Developers</a>. What I intend for this article is provide a simple and cleaner migration alternative off of the <a href="https://github.com/angular/flex-layout" target="_blank" rel="nofollow"><code>@angular/flex-layout</code></a> package. The migration is actually a step back in time to utilize a set of CSS Flexbox classes defined for AngularJS and I need to give a shout-out to my team member, <a href="https://pamtingiris.com/my-work" target="_blank" rel="nofollow">Pam Tingiris</a>, for remembering and formulating this solution.</p>
<p>I am a solutions architect over a team of developers. During the past few years we have developed a number of Angular applications that are farily large and involved. When we started we were obviously using an older version of Angular without the <a href="https://github.com/angular/flex-layout" target="_blank" rel="nofollow"><code>@angular/flex-layout</code></a> package. We then followed the Angular team's recommendation to utilize the <code>@angular/flex-layout</code> package so we migrated to it. While the migration was time consuming and tedious just due to the volume of changes to make, it was actually fairly straight-forward and was simply the inverse of what I'm recommended for you today. Fortunately, in our code repository we were able to go back and find the old AngularJS flex layout classes that we had previously migrated away from and deleted. I'm included it at the end of this post.</p>
<p>The migration is mostly a straight conversion from the <a href="https://github.com/angular/flex-layout" target="_blank" rel="nofollow"><code>@angular/flex-layout</code></a> injected DOM stylings to CSS classes. For example, let's look at the initial example provided on the <a href="https://github.com/angular/flex-layout" target="_blank" rel="nofollow"><code>@angular/flex-layout</code></a> GitHub page with an addition of a fxFlex div child:</p>
<pre><div fxLayout="row" fxLayoutAlign="space-between">
<div fxFlex></div>
</div></pre>
<p>With the CSS classes, that simply becomes:</p>
<pre><div class="layout-row layout-align-space-between">
<div class="flex"></div>
</div></pre>
<p>As you can see, it's a pretty straight-forward process. When you review the CSS at the bottom of the page, you will see there are classes for most every layout variation you might use. For example, <code>fxLayoutAlign="space-between center"</code> becomes <code>class="layout-align-space-between-center"</code> and <code>fxFlex="50"</code> becomes <code>class="flex-50"</code>.</p>
<p>For the most part, we standardized our layout gaps to 1rem (example <code>fxLayoutGap="1rem"</code>) so we added a single <code>layout-gap</code> class at the end of the original CSS file (<code>.layout-gap { gap: 1rem }</code>), but for the few occasions when we needed different spacing in a component we'd just add a custom class in the component's CSS such as <code>.layout-gap-3rem { gap: 3rem }</code>.</p>
<p>Now, you might be thinking to yourself, that's all well and good for basic flex layout directives, but what about this part of flex-layout?</p>
<blockquote>The real power of Flex Layout, however, is its responsive engine. The Responsive API enables developers to easily specify different layouts, sizing, visibilities for different viewport sizes and display devices.</blockquote>
<p>Here's where it's a little harder, but still not terrible. We'll utilize the <a href="https://material.angular.io/cdk/layout/overview" target="_blank" rel="nofollow">Material CDK layout package</a> to create a <a href="https://material.angular.io/cdk/layout/overview#breakpointobserver" target="_blank" rel="nofollow">BreakpointObserver</a> service.</p>
<p>You inject this service into any component that needs media queries for responsive displays.</p>
<pre> constructor(
public breakpoints: BreakpointsService,
) { }
</pre>
<p>Now, if you have a page that is laid out in rows, but you want it to switch to columns for a phone display, you might have something like:</p>
<pre><div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="space-between">
</div></pre>
<p>Utilizing the breakpoints, this will become:</p>
<pre><div class="layout layout-align-space-between"> [ngClass]="{'layout-column' : breakpoints.screen('xs')}">
</div></pre>
<p>Admittedly not as nice as the concise <code>fxLayout.xs</code>, but it does the job. The built in breakpoint names of the BreakpointObserver like <code>XSmall</code> don't correspond to the flex-layout names like <code>xs</code>, so we named them to match. Our BreakpointsService looks like:</p>
<pre>/********************************************************************************
* BreakpointsService (a BreakpointObserver)
*
* Implementation of the Material CDK BreakpointObserver
* see https://material.angular.io/cdk/layout/overview#breakpointobserver
* This defines a set of viewport breakpoints (media queries) that allow us
* to react to changes in viewport sizes for a responsive UI.
********************************************************************************/
import { Injectable, OnDestroy } from '@angular/core';
import { BreakpointObserver, Breakpoints} from '@angular/cdk/layout';
import { takeUntil } from 'rxjs/operators';
import { BehaviorSubject, Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class BreakpointsService implements OnDestroy {
destroyed = new Subject<void>();
mediaQueryMap = new Map([
['xs', Breakpoints.XSmall ],
['sm', Breakpoints.Small],
['md', Breakpoints.Medium],
['lg', Breakpoints.Large],
['xl', Breakpoints.XLarge],
['gt-xs', '(min-width: 600px)'],
['gt-sm', '(min-width: 960px)'],
['gt-md', '(min-width: 1280px)'],
]);
protected matchesSubject = new BehaviorSubject([]);
matches = this.matchesSubject.asObservable();
constructor(public breakpointObserver: BreakpointObserver) {
breakpointObserver
.observe([
Breakpoints.XSmall,
Breakpoints.Small,
Breakpoints.Medium,
Breakpoints.Large,
Breakpoints.XLarge,
'(min-width: 600px)',
'(min-width: 960px)',
'(min-width: 1280px)',
])
.pipe(takeUntil(this.destroyed))
.subscribe(result => {
const matchedBreakpoints = Object.entries( result.breakpoints ).filter( e => e[1] );
this.matchesSubject.next(matchedBreakpoints);
});
}
ngOnDestroy() {
this.destroyed.next();
this.destroyed.complete();
}
/***
* Screen sizes for responsive styling
* @param size 'xs', 'gt-xs'
* */
screen(size) {
let screenSize = false;
const matches = this.matches.subscribe(m => {
const mq = this.mediaQueryMap.get(size);
screenSize = m.filter( match => match[0] === mq ).length === 1;
} );
return screenSize;
}
}
</pre>
<p>And that's it! Now it's just a process of finding all the "fx" directives and any screen size rules, and swapping them out for classes. Once you remove the <code>FlexLayoutModule</code> from your <code>app.module.ts</code>, you'll quickly discover anything you might have missed.</p>
<pre><span style="text-decoration: line-through;">import { FlexLayoutModule } from '@angular/flex-layout';</span>
@NgModule({
imports: [
<span style="text-decoration: line-through;">FlexLayoutModule,</span>
</pre>
<p>In conclusion, we found this migration path to be largely a 1-to-1 replacement. We've seen so many other packages come into favor and then fall out of favor over time (like <a href="https://getbootstrap.com/" target="_blank" rel="nofollow">bootstrap</a>) that we didn't want to go down the route of switching to <a href="https://tailwindcss.com/" target="_blank" rel="nofollow">TailwindCSS</a> just to have it ripped out from under us again in a few years. Hopefully you find this to be a helpful alternative.</p>
<h4>Flex Layout CSS classes</h4>
<pre>/* flex layout classes - instructions https://material.angularjs.org/latest/layout/introduction */
.layout-align, .layout-align-start-stretch {
justify-content: flex-start;
align-content: stretch;
align-items: stretch
}
.layout-align-start, .layout-align-start-center, .layout-align-start-end, .layout-align-start-start, .layout-align-start-stretch {
justify-content: flex-start
}
.layout-align-center, .layout-align-center-center, .layout-align-center-end, .layout-align-center-start, .layout-align-center-stretch {
justify-content: center
}
.layout-align-end, .layout-align-end-center, .layout-align-end-end, .layout-align-end-start, .layout-align-end-stretch {
justify-content: flex-end
}
.layout-align-space-around, .layout-align-space-around-center, .layout-align-space-around-end, .layout-align-space-around-start, .layout-align-space-around-stretch {
justify-content: space-around
}
.layout-align-space-between, .layout-align-space-between-center, .layout-align-space-between-end, .layout-align-space-between-start, .layout-align-space-between-stretch {
justify-content: space-between
}
.layout-align-space-evenly, .layout-align-space-evenly-center, .layout-align-space-evenly-end, .layout-align-space-evenly-start, .layout-align-space-evenly-stretch {
justify-content: space-evenly
}
.layout-align-center-start, .layout-align-end-start, .layout-align-space-around-start, .layout-align-space-between-start, .layout-align-start-start, .layout-align-space-evenly-start {
align-items: flex-start;
align-content: flex-start
}
.layout-align-center-center, .layout-align-end-center, .layout-align-space-around-center, .layout-align-space-between-center, .layout-align-start-center, .layout-align-space-evenly-center {
align-items: center;
align-content: center;
max-width: 100%
}
.layout-align-center-center > *, .layout-align-end-center > *, .layout-align-space-around-center > *, .layout-align-space-between-center > *, .layout-align-start-center > *, .layout-align-space-evenly-center > * {
max-width: 100%;
box-sizing: border-box
}
.layout-align-center-end, .layout-align-end-end, .layout-align-space-around-end, .layout-align-space-between-end, .layout-align-start-end, .layout-align-space-evenly-end {
align-items: flex-end;
align-content: flex-end
}
.layout-align-center-stretch, .layout-align-end-stretch, .layout-align-space-around-stretch, .layout-align-space-between-stretch, .layout-align-start-stretch, .layout-align-space-evenly-stretch {
align-items: stretch;
align-content: stretch
}
.flex {
flex: 1
}
.flex, .flex-grow {
box-sizing: border-box
}
.flex-grow {
flex: 1 1 100%
}
.flex-initial {
flex: 0 1 auto;
box-sizing: border-box
}
.flex-auto {
flex: 1 1 auto;
box-sizing: border-box
}
.flex-none {
flex: 0 0 auto;
box-sizing: border-box
}
.flex-noshrink {
flex: 1 0 auto;
box-sizing: border-box
}
.flex-nogrow {
flex: 0 1 auto;
box-sizing: border-box
}
.flex-0, .layout-row > .flex-0 {
flex: 1 1 100%;
max-width: 0;
max-height: 100%;
box-sizing: border-box
}
.layout-row > .flex-0 {
min-width: 0
}
.layout-column > .flex-0 {
flex: 1 1 100%;
max-width: 100%;
max-height: 0%;
box-sizing: border-box
}
.flex-5, .layout-row > .flex-5 {
flex: 1 1 100%;
max-width: 5%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-5 {
flex: 1 1 100%;
max-width: 100%;
max-height: 5%;
box-sizing: border-box
}
.flex-10, .layout-row > .flex-10 {
flex: 1 1 100%;
max-width: 10%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-10 {
flex: 1 1 100%;
max-width: 100%;
max-height: 10%;
box-sizing: border-box
}
.flex-15, .layout-row > .flex-15 {
flex: 1 1 100%;
max-width: 15%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-15 {
flex: 1 1 100%;
max-width: 100%;
max-height: 15%;
box-sizing: border-box
}
.flex-20, .layout-row > .flex-20 {
flex: 1 1 100%;
max-width: 20%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-20 {
flex: 1 1 100%;
max-width: 100%;
max-height: 20%;
box-sizing: border-box
}
.flex-25, .layout-row > .flex-25 {
flex: 1 1 100%;
max-width: 25%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-25 {
flex: 1 1 100%;
max-width: 100%;
max-height: 25%;
box-sizing: border-box
}
.flex-30, .layout-row > .flex-30 {
flex: 1 1 100%;
max-width: 30%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-30 {
flex: 1 1 100%;
max-width: 100%;
max-height: 30%;
box-sizing: border-box
}
.flex-35, .layout-row > .flex-35 {
flex: 1 1 100%;
max-width: 35%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-35 {
flex: 1 1 100%;
max-width: 100%;
max-height: 35%;
box-sizing: border-box
}
.flex-40, .layout-row > .flex-40 {
flex: 1 1 100%;
max-width: 40%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-40 {
flex: 1 1 100%;
max-width: 100%;
max-height: 40%;
box-sizing: border-box
}
.flex-45, .layout-row > .flex-45 {
flex: 1 1 100%;
max-width: 45%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-45 {
flex: 1 1 100%;
max-width: 100%;
max-height: 45%;
box-sizing: border-box
}
.flex-50, .layout-row > .flex-50 {
flex: 1 1 100%;
max-width: 50%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-50 {
flex: 1 1 100%;
max-width: 100%;
max-height: 50%;
box-sizing: border-box
}
.flex-55, .layout-row > .flex-55 {
flex: 1 1 100%;
max-width: 55%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-55 {
flex: 1 1 100%;
max-width: 100%;
max-height: 55%;
box-sizing: border-box
}
.flex-60, .layout-row > .flex-60 {
flex: 1 1 100%;
max-width: 60%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-60 {
flex: 1 1 100%;
max-width: 100%;
max-height: 60%;
box-sizing: border-box
}
.flex-65, .layout-row > .flex-65 {
flex: 1 1 100%;
max-width: 65%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-65 {
flex: 1 1 100%;
max-width: 100%;
max-height: 65%;
box-sizing: border-box
}
.flex-70, .layout-row > .flex-70 {
flex: 1 1 100%;
max-width: 70%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-70 {
flex: 1 1 100%;
max-width: 100%;
max-height: 70%;
box-sizing: border-box
}
.flex-75, .layout-row > .flex-75 {
flex: 1 1 100%;
max-width: 75%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-75 {
flex: 1 1 100%;
max-width: 100%;
max-height: 75%;
box-sizing: border-box
}
.flex-80, .layout-row > .flex-80 {
flex: 1 1 100%;
max-width: 80%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-80 {
flex: 1 1 100%;
max-width: 100%;
max-height: 80%;
box-sizing: border-box
}
.flex-85, .layout-row > .flex-85 {
flex: 1 1 100%;
max-width: 85%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-85 {
flex: 1 1 100%;
max-width: 100%;
max-height: 85%;
box-sizing: border-box
}
.flex-90, .layout-row > .flex-90 {
flex: 1 1 100%;
max-width: 90%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-90 {
flex: 1 1 100%;
max-width: 100%;
max-height: 90%;
box-sizing: border-box
}
.flex-95, .layout-row > .flex-95 {
flex: 1 1 100%;
max-width: 95%;
max-height: 100%;
box-sizing: border-box
}
.layout-column > .flex-95 {
max-height: 95%
}
.flex-100, .layout-column > .flex-95 {
flex: 1 1 100%;
max-width: 100%;
box-sizing: border-box
}
.flex-100 {
max-height: 100%
}
.layout-column > .flex-100, .layout-row > .flex-100 {
flex: 1 1 100%;
max-width: 100%;
max-height: 100%;
box-sizing: border-box
}
.flex-33 {
max-width: 33.33%
}
.flex-33, .flex-66 {
flex: 1 1 100%;
max-height: 100%;
box-sizing: border-box
}
.flex-66 {
max-width: 66.66%
}
.layout-row > .flex-33 {
flex: 1 1 33.33%
}
.layout-row > .flex-66 {
flex: 1 1 66.66%
}
.layout-column > .flex-33 {
flex: 1 1 33.33%
}
.layout-column > .flex-66 {
flex: 1 1 66.66%
}
.layout-row > .flex-33 {
max-width: 33.33%
}
.layout-row > .flex-33, .layout-row > .flex-66 {
flex: 1 1 100%;
max-height: 100%;
box-sizing: border-box
}
.layout-row > .flex-66 {
max-width: 66.66%
}
.layout-row > .flex {
min-width: 0
}
.layout-column > .flex-33 {
max-height: 33.33%
}
.layout-column > .flex-33, .layout-column > .flex-66 {
flex: 1 1 100%;
max-width: 100%;
box-sizing: border-box
}
.layout-column > .flex-66 {
max-height: 66.66%
}
.layout-column > .flex {
min-height: 0
}
.layout, .layout-column, .layout-row {
box-sizing: border-box;
display: flex
}
.layout-column {
flex-direction: column
}
.layout-row {
flex-direction: row
}
.layout-padding-sm > *, .layout-padding > .flex-sm {
padding: 4px
}
.layout-padding, .layout-padding-gt-sm, .layout-padding-gt-sm > *, .layout-padding-md, .layout-padding-md > *, .layout-padding > *, .layout-padding > .flex, .layout-padding > .flex-gt-sm, .layout-padding > .flex-md {
padding: 8px
}
.layout-padding-gt-lg > *, .layout-padding-gt-md > *, .layout-padding-lg > *, .layout-padding > .flex-gt-lg, .layout-padding > .flex-gt-md, .layout-padding > .flex-lg {
padding: 16px
}
.layout-margin-sm > *, .layout-margin > .flex-sm {
margin: 4px
}
.layout-margin, .layout-margin-gt-sm, .layout-margin-gt-sm > *, .layout-margin-md, .layout-margin-md > *, .layout-margin > *, .layout-margin > .flex, .layout-margin > .flex-gt-sm, .layout-margin > .flex-md {
margin: 8px
}
.layout-margin-gt-lg > *, .layout-margin-gt-md > *, .layout-margin-lg > *, .layout-margin > .flex-gt-lg, .layout-margin > .flex-gt-md, .layout-margin > .flex-lg {
margin: 16px
}
.layout-wrap {
flex-wrap: wrap
}
.layout-nowrap {
flex-wrap: nowrap
}
.layout-fill {
margin: 0;
width: 100%;
min-height: 100%;
height: 100%
}
.layout-gap { gap: 1rem }
</pre>Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-21222625.post-67453111775960469952023-01-06T16:27:00.004-05:002023-02-13T12:30:10.767-05:00Reporting errors to Slack from Java Spring Boot and .NET Framework applications<p>Watching server logs for errors is a cumbersome way to find problems in server code. </p><p>Back when I was actively doing Ruby on Rails for personal development, I used the <a href="https://rubygems.org/gems/exceptional/versions/2.0.33" rel="nofollow" target="_blank">"exceptional" gem</a> which would report exceptions to a now defunct online service (exceptional.io) to monitor any errors that occurred in your application. It was a nice tool, but it had 2 drawbacks for me. </p><p></p><ol style="text-align: left;"><li>It was just for Ruby on Rails</li><li>It required a 3rd party site to store the exception data. </li></ol><p></p><p>At the time, I was working at IBM so I needed a tool that worked for Java applications, and they didn't look kindly on potentially confidential information being stored outside of the IBM network, so I created my own version of Exception that I named IBMExceptional (you can read a little bit about it and see some screenshots on my <a href="https://www.stevenpothoven.com/#/project/ibm" rel="nofollow" target="_blank">IBM portfolio page</a>). The site for monitoring the errors was a Rails application, but I created a Java client for it that provided 2 reporting options. </p><p><b>First</b>, I provided an IBMExceptional exception class that could be extended for application specific exceptions. </p>
<pre> class MyProjectException extends IBMExceptional {
@SuppressWarnings("unused")
public MyProjectException() {
super();
}
public MyProjectException(String message, Throwable cause) {
super(message, cause);
}
public MyProjectException(String message) {
super(message);
}
@SuppressWarnings("unused")
public MyProjectException(Throwable cause) {
super(cause);
}
}</pre>
<p>This base exception class would report any instances of thrown exceptions to IBMExceptional.</p>
<pre> try {
throw new MyProjectException("This is a test exception.");
} catch (MyProjectException e) {
// Handle exception as desired, but it was reported to IBM Exceptional
}
</pre>
<p>The advantage of this option is that the exception reporting is transparent and automatic. The disadvantage is that only exceptions that are subclassed from the IBMExceptional base class will be reported. </p><p><b>Second</b>, I provided a log appender, IBMExceptionalAppender, so that any logged errors would be reported to IBMExceptional. You added a few configuration lines to log4j.properties like</p>
<pre> log4j.rootCategory=info, exceptional
log4j.appender.exceptional=com.ibm.IBMExceptionalAppender
</pre>
<p>Then calls to the logger with an exception would be reported to IBMExceptional.</p>
<pre> try {
throw new NullPointerException("Fake Null pointer");
} catch (NullPointerException e) {
logger.error("Error description", e);
}
</pre>
<p>The advantage of this option is that all exception types can be reported. The disadvantage is that it requires that the exceptions are logged by the programmer.</p>
<p>This worked well and was all well and good while I remained at IBM, but I have since moved on. For my current job at USIC, I've led the development of a Java API server using Spring Boot (feeding Angular UIs) and a C# .NET (Framework) application and decided I would similarly like to easily monitor when errors occur on the servers, but this time, instead of having some web site to monitor the errors, I thought it would be much simpler and be sufficient to just send them to dedicated Slack channels. </p><p>For Java we went the route of a log appender, for .NET we could tie into the <code>System.Web.HttpApplication Application_Error</code> handler.</p>
<h2 style="text-align: left;">Java Spring Boot application with log appender</h2>
<p>Spring Boot uses logback rather than log4j, so in <code>logback-spring.xml</code> we defined a new appender for errors.</p>
<pre> <appender name="SLACK" class="com.usicllc.jsonapi.log.SlackAppender">
<profile>${profile}</profile>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
</pre>
<p>Then the <code>SlackAppender</code> class utilizes the JSlack library and looks like:</p>
<pre>public class SlackAppender extends AppenderBase<ILoggingEvent> {
@Override
protected void append(ILoggingEvent event) {
String errorMessage = event.getFormattedMessage();
StringBuffer message = new StringBuffer("*Error: " + errorMessage + "*\n\n");
String att = null;
Map<String, String> mdcMap = event.getMDCPropertyMap();
if (mdcMap != null) {
String reqUrl = mdcMap.get("REQUEST_URL");
String reqMethod = mdcMap.get("REQUEST_METHOD");
String reqQueryString = mdcMap.get("REQUEST_QUERY_STRING");
String reqUserAgent = mdcMap.get("REQUEST_USER_AGENT");
String reqRealIp = mdcMap.get("REQUEST_X_REAL_IP");
String reqUser = mdcMap.get("REQUEST_USER");
String reqBody = mdcMap.get("REQUEST_BODY");
if (reqMethod != null) {
message.append("*REQUEST_METHOD:* " + reqMethod + "\n");
message.append("*REQUEST_URL:* " + reqUrl + "\n");
if (reqQueryString != null) {
try {
message.append("*REQUEST_QUERY_STRING:* " + URLDecoder.decode(reqQueryString, "UTF-8") + "\n");
} catch (UnsupportedEncodingException e) {
}
}
if (reqBody != null) {
message.append("*REQUEST_BODY:* " + reqBody + "\n");
}
message.append("*REQUEST_USER_AGENT:* " + reqUserAgent + "\n");
message.append("*REQUEST_X_REAL_IP:* " + reqRealIp + "\n");
message.append("*REQUEST_USER:* " + reqUser + "\n");
}
}
// If we have an exception being reported, then add it to the message
//
if (event.getThrowableProxy() != null) {
StackTraceElementProxy[] stArray = event.getThrowableProxy().getStackTraceElementProxyArray();
if (stArray.length > 0) {
// first 10 lines of the stack trace are added to the messages as a code block
// Full stack trace is also added as an attachment
message.append("*Stack Trace:*\n```");
String exceptionMessage = event.getThrowableProxy().getMessage();
StringBuffer stackTrace = new StringBuffer("*Error Message: " + exceptionMessage + "*\n");
for (int i = 0; i < stArray.length; i++) {
StackTraceElement t = stArray[i].getStackTraceElement();
String traceLine = t.toString();
if (i < 10) {
// 1st 10 lines are added to the main message in a code block
message.append(traceLine + "\n");
}
if (traceLine.startsWith("com.usicllc.jsonapi")) {
// emphasize where the error was in our code
stackTrace.append("*" + traceLine + "*\n");
} else {
stackTrace.append(traceLine + "\n");
}
}
if (stArray.length > 10) {
message.append("...\n");
// Full stack trace is more than the 10 lines shown (usually the case)
// Add the full stacktrace as an attachment
Attachment attachment = Attachment.builder()
.title("Full Stack Trace")
.text(stackTrace.toString())
.build();
att = "[{\"title\" :\""+attachment.getTitle()+":\n"+"\",\"text\": \""+attachment.getText().replace("\\", "\\\\")+"\"}]";
}
message.append("```");
}
}
String payload = "{\"channel\": \"" + this.getSlackChannel()
+ "\",\"username\": \"jsonapi\",\"text\": \""
+ message.toString().replace("\\", "\\\\")
+ "\",\"attachments\":" + att + "}";
WebhookResponse webhookResponse;
try {
webhookResponse = Slack.getInstance().send(
this.getSlackUrl(), payload);
if (webhookResponse.getCode() != 200) {
logger.debug("Unable to send Slack Exception - http error:" + webhookResponse.toString());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
</pre>
<p>The stack trace is added both with the first 10 lines in a code block in the message body and then the full stack trace as an attachment because its more readable as a code block due to no line wrapping and the use of a monospaced font, but Slack will limit the message size and most stack traces will exceed the limit so adding the full stack trace an attachment provides access to everything albeit in a less readable format. </p><p>Now, it could have just logged the exception message and stack trace, but it's certainly more helpful to know what request caused the problem and that's where we took advantage of the logback MDC (Mapped Diagnoatic Context) to retrieve request data in a Spring <code>GenericFilterBean</code> named <code>PreRequestProcessingFilter</code> Spring will inject this into the request filter chain and the filter code looks like:</p>
<pre>@Component
public class PreRequestProcessingFilter extends GenericFilterBean implements CurrentUser {
Logger logger = LoggerFactory.getLogger(PreRequestProcessingFilter.class);
@SuppressWarnings("unchecked")
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) {
try {
String bodyJson = null;
RequestWrapper wrapper = null;
String method = ((HttpServletRequest) req).getMethod();
String contentType = ((HttpServletRequest) req).getContentType();
// Get a copy of the request body for JSON POST and PATCH requests
if (contentType != null &&
(contentType.startsWith("application/json") || contentType.startsWith("application/vnd.api+json")) &&
(method.equalsIgnoreCase("POST") || method.equalsIgnoreCase("PATCH"))) {
wrapper = new RequestWrapper((HttpServletRequest) req);
byte[] body = StreamUtils.copyToByteArray(wrapper.getInputStream());
Map<String, Object> jsonRequest = new ObjectMapper().readValue(body, Map.class);
bodyJson = jsonRequest.toString();
}
String user = getUserName();
StringBuffer requestURL = ((HttpServletRequest) req).getRequestURL();
if (requestURL != null) {
MDC.put("REQUEST_URL", requestURL.toString());
}
MDC.put("REQUEST_METHOD", method);
MDC.put("REQUEST_QUERY_STRING", ((HttpServletRequest) req).getQueryString());
MDC.put("REQUEST_USER_AGENT", ((HttpServletRequest) req).getHeader("User-Agent"));
MDC.put("REQUEST_X_REAL_IP", ((HttpServletRequest) req).getHeader("X-Real-IP"));
MDC.put("REQUEST_USER", user);
MDC.put("REQUEST_BODY", bodyJson);
filterChain.doFilter((wrapper != null ? wrapper : req), res);
} catch (IOException | ServletException e) {
e.printStackTrace();
} finally {
MDC.clear();
}
}
}
</pre>
<p>The big trick in that filter is obtaining the POST or PATCH request body without consuming it and thereby prevening the regular request processing from getting it. Normally, you can only read the request body once. So we create a <code>RequestWrapper</code> and a <code>ServletInputStreamWrapper</code> which allow us to extract a copy of the request's input stream (body). The code for these classes looks like:</p>
<pre>public class RequestWrapper extends HttpServletRequestWrapper {
private byte[] body;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.body = StreamUtils.copyToByteArray(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new ServletInputStreamWrapper(this.body);
}
}
public class ServletInputStreamWrapper extends ServletInputStream {
private InputStream inputStream;
public ServletInputStreamWrapper(byte[] body) {
this.inputStream = new ByteArrayInputStream(body);
}
@Override
public boolean isFinished() {
try {
return inputStream.available() == 0;
} catch (Exception e) {
return false;
}
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
// no need to implment this method
}
@Override
public int read() throws IOException {
return this.inputStream.read();
}
}
</pre>
<p>With all this in place, we now get nice Slack messages for errors providing request data and stack trace. The final result looks like:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBWN9hZtXlMDA5Rfta3sZWnP262fyBQkWHKZTXKCcDfsNuU7jPHRHKEJDRm3nug41fD6NInf5r6KHPQjq2Z2VnL5zBPpsPyHgrM-yG5pZkbcsRzQyP4_c_Uougobx2NnDCR3KdPXEI2h3J_SgJtGKf_4Bx7hX2CynSzC8g0MjlpNQbKnmrNQ/s834/SampleSlackError.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="496" data-original-width="834" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBWN9hZtXlMDA5Rfta3sZWnP262fyBQkWHKZTXKCcDfsNuU7jPHRHKEJDRm3nug41fD6NInf5r6KHPQjq2Z2VnL5zBPpsPyHgrM-yG5pZkbcsRzQyP4_c_Uougobx2NnDCR3KdPXEI2h3J_SgJtGKf_4Bx7hX2CynSzC8g0MjlpNQbKnmrNQ/s16000/SampleSlackError.png" /></a></div><br /><p><br /></p>
<h2 style="text-align: left;">.NET Framework Application with Application_Error handler</h2>
<p>For the .NET Framework application its much simpler. In our <code>Global.asax.cs</code> file (where we create our application subclass of <code>System.Web.HttpApplication</code>) we define the <code>Application_Error</code> hook which allows us access to the request data, and then we simply send it to Slack utilizing the <code><a href="https://github.com/mrb0nj/Slack.Webhooks">Slack.Webhooks</a></code> package.</p>
<pre> public class MvcApplication : System.Web.HttpApplication
{
private static readonly bool slackEnabled = Convert.ToBoolean(ConfigurationManager.AppSettings["slackEnabled"]);
private static readonly string slackURL = ConfigurationManager.AppSettings["slackURL"];
private static readonly string slackChannel = ConfigurationManager.AppSettings["slackChannel"];
private static readonly SlackClient slackClient = new SlackClient(slackURL);
private static readonly string environment = ConfigurationManager.AppSettings["environmentMode"];
protected void Application_Error(object sender, EventArgs e)
{
Exception ex = null;
// make sure whatever we do here doesn't generate any exceptions and generate a loop
try
{
ex = Server.GetLastError();
string requestType = HttpContext.Current.Request.RequestType;
string requestURL = Convert.ToString(HttpContext.Current.Request.Url);
string requestReferrer = Convert.ToString(HttpContext.Current.Request.UrlReferrer);
string requestBrowser = HttpContext.Current.Request.Browser.Browser + " " + HttpContext.Current.Request.Browser.Version;
string requestPlatform = HttpContext.Current.Request.Browser.Platform;
bool requestIsAuthenticated = HttpContext.Current.Request.IsAuthenticated;
string requestUserAgent = HttpContext.Current.Request.UserAgent;
string currentUser = User.Identity.Name?.Split('\\')[1];
string requestUserAddress = HttpContext.Current.Request.UserHostAddress;
string requestForwardedForAddress = HttpContext.Current.Request.Headers.Get("X-Forwarded-For");
Stream requestInputStream = HttpContext.Current.Request.InputStream;
requestInputStream.Seek(0, SeekOrigin.Begin);
string requestBody = new StreamReader(requestInputStream).ReadToEnd();
string logInfoString =
"\n User: " + currentUser
+ "\n Request type: " + requestType
+ "\n Request URL: " + requestURL
+ "\n Referrer: " + requestReferrer
+ "\n Browser: " + requestBrowser
+ "\n Platform: " + requestPlatform
+ "\n UserAgent: " + requestUserAgent
+ (requestIsAuthenticated ? "" : ("\n IsAuthenticated: " + requestIsAuthenticated))
+ "\n Source address: " + requestUserAddress + (string.IsNullOrEmpty(requestForwardedForAddress) ? "" :
(" / " + requestForwardedForAddress));
string logInfoString = requestInfoString +
(string.IsNullOrEmpty(requestBody) ? "" : ("\n Request body: " + requestBody));
log.Error(logInfoString, ex);
SendSlackMessage("Application Error:", requestInfoString, ex, requestBody);
}
catch (Exception exc) {
// If we have issues getting any of the info above, at least log the exception
try
{
log.Error("Original exception: ", ex);
log.Error("Additionally, there was an exception in Application_Error: " + exc);
}
catch (Exception) { }
}
}
public static void SendSlackMessage(string title, string message, Exception exception, string requestBody)
{
// don't send to slack if disabled or running locally
if (!slackEnabled || environment == "local") return;
// add time and make the title bold so that messages posted in quick succession can be easily told apart
// (*stuff* makes it bold)
string slackText = "*" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + (title != null ? (" - " + title) : "") + "*";
if (message != null)
{
slackText += "\n" + message;
}
List<SlackAttachment> slackAttachments = new List<SlackAttachment>();
if (!string.IsNullOrWhiteSpace(requestBody))
{
var requestBodyAttachment = new SlackAttachment
{
Text = requestBody
};
slackAttachments.Add(requestBodyAttachment);
}
if (exception != null)
{
var slackAttachment = new SlackAttachment
{
Text = exception.ToString(),
Color = "#C20202"
};
slackAttachments = new List<SlackAttachment> { slackAttachment };
}
// local defaults. Comment out first line of the method to test locally if needed
string slackUser = ".NET Application";
string slackUserEmoji = Emoji.Wrench;
var slackMessage = new SlackMessage
{
Channel = slackChannel,
Text = slackText,
IconEmoji = slackUserEmoji,
Username = slackUser,
Attachments = slackAttachments
};
slackClient.PostAsync(slackMessage);
}
</pre>
<p><br /></p><p><br /></p>
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-21222625.post-74230237576454112792021-06-28T13:18:00.002-04:002021-06-28T13:18:41.210-04:00The CVS ExtraCare Coupon Clipper<p>In the same spirit as the <a href="https://blog.pothoven.net/2021/06/return-of-publix-coupon-clipper.html">Publix Digital Coupon Clipper</a> for which I've received a lot of positive feedback and requests for similar functions for other stores, I also announce the CVS ExtraCare coupon clipper. These digital coupon sites don't like to provide a "Clip All" function. I presume this is for some advertising purpose so you're forced to look at each one. I like to have all the coupons clipped and if I happen to buy something there's a coupon it will just apply it at checkout. </p><p>Plus with CVS being notorious for spitting out a whole roll of receipt paper with printed coupons, if they're all pre-clipped electronically on your CVS ExtraCare card I don't believe it will print them so you can save some trees. </p><div><b>Please go to <a href="https://pothoven.net/CvsClipper.html" style="color: #de7008;">https://pothoven.net/CvsClipper.html</a> for installation instructions</b>, and please drop me a comment here if you try it out to let me know how it works for you.</div>Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-21222625.post-10283419717826470102021-06-28T12:18:00.001-04:002021-06-28T12:19:57.365-04:00Return of the Publix Coupon Clipper<p>My Publix Coupon Clipper bookmarklet has been offline for a while now due to changes that had been made to the to website code to disable it. I like to think the changes made were specifically targeted at my bookmarklet, but it may have been purely coincidental too. Regardless, my handy clipper was rendered inoperable with no way to circumvent what they had done to disable it.</p><p>For those reading this who are unfamiliar with it, I introduced the coupon clipper in <a href="https://blog.pothoven.net/2014/01/clip-all-function-for-publix-digital.html">January 2014</a> as a "Clip All" function that was (and still is) missing from the site to quickly and easily clip all the digital coupons so they'll all be ready and waiting when you checkout. I revised it <a href="https://blog.pothoven.net/2015/11/updated-clip-all-function-for-publix.html">November 2015</a> to deal with some page changes, but in December of 2016 a change was added to the site that permanently disabled it.</p><p><b>Fortunately for us</b>, they've re-written the coupon site using Vue instead of JQuery and the code that prevented the coupon clipper from working did not make the transition. I've made the necessary changes to make it work with the new page, and it's working again! So while it lasts...</p><div style="text-align: left;"><b>Please go to <a href="https://pothoven.net/PublixClipper.html" style="color: #de7008;">https://pothoven.net/PublixClipper.html</a> for installation instructions</b>, and please drop me a comment here if you try it out to let me know how it works for you.</div>Unknownnoreply@blogger.com2tag:blogger.com,1999:blog-21222625.post-29333805224124026742020-12-09T10:29:00.001-05:002020-12-09T10:29:05.623-05:00Angular Clipboard ServiceIt's about time I post something new! Since my last post I've had to learn and build applications in the Ember, React, and now Angular JavaScript Frameworks. Along the way I've learned some interesting tips and tricks I can share here.<div><br /></div><div>I needed the ability to place content in the clipboard, but needed it lots of placed throughout the application. So here's a simple Clipboard service that will place any string into the clipboard. I incorporated the use of <a href="https://material.angular.io/components/snack-bar/overview" target="_blank">MatSnackBar </a>to notify the user that the value has been placed in the clipboard.</div><div><br /></div>
<pre>import {Injectable, Renderer2, RendererFactory2} from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
@Injectable({
providedIn: 'root'
})
export class ClipboardService {
private renderer: Renderer2;
constructor (
private rendererFactory: RendererFactory2,
private snackBar: MatSnackBar,
) {
// Get an instance of Angular's Renderer2
this.renderer = this.rendererFactory.createRenderer(null, null);
}
copy(value: string) {
// create an temporary text input field to contain the value and select the value
const input = this.renderer.createElement('input');
this.renderer.setProperty(input, 'value', value);
this.renderer.appendChild(document.body, input);
input.focus();
input.select();
input.setSelectionRange(0, 99999); // For mobile devices
// Copy the selected text inside the text field to the clipboard
document.execCommand('copy');
this.snackBar.open(`Copied "${value}" to clipboard`, undefined, { duration: 1000 });
// remove the temporary text input field
this.renderer.removeChild(document.body, input);
}
}
</pre><div><br /></div>
You then simply inject it into any component that needs it:<div><br /></div>
<pre>import { ClipboardService } from 'clipboard.service';
constructor( private clipboard: ClipboardService ) {} </pre><div><br /></div>
and then copy whatever values you want to the clipboard simply as:
<pre>this.clipboard.copy(value);</pre>
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-21222625.post-30264411774208315432019-05-13T14:30:00.000-04:002019-05-13T14:30:36.540-04:002's complement and BCD hex values in JavaScriptI'm sure this is will not be a highly accessed post as these are not a commonly required functions, but in my current job, we process data from IoT GPS devices which send data in compact formats. To process this data using Node.js in an AWS Lambda function, I had the need to convert a <a href="https://en.wikipedia.org/wiki/Two%27s_complement" target="_blank">2's complement</a> hex values and BCD (<a href="https://en.wikipedia.org/wiki/Binary-coded_decimal" target="_blank">binary-coded decimal</a>) hex value to regular numbers in JavaScript. The examples I found were in Java, so I converted it to JavaScript and am sharing here in case anyone else needs it.<br />
<br />
<pre> const negativeHexTestPattern = /^[89ABCDEF]/i
/* parse signed 2's complement hex string to number */
hexToTwosComplement(hex) {
let result = parseInt(hex, 16)
// Check if high bit is set.
if (negativeHexTestPattern.test(hex)) {
// Negative number
const subtrahend = (2 ** (hex.length * 4))
result -= subtrahend
}
return result
}
/* parse packed binary-coded decimal (BCD) format where unused
* trailing digits (4-bits) are filled with all ones (1111). */
hexBCDToString(hex) {
let decoded = ''
for (let i = 0; i < hex.length; i++) {
if ((parseInt(hex[i],16) & 0x0F) !== 0x0F) {
decoded += hex[i]
} else {
break
}
}
return decoded
}
</pre>
<br />
Here's a few test cases to demonstrate.
<br />
<pre> it('converts positive hex values to twos complement', () => {
const val = new Processor().hexToTwosComplement('13f6b4eb')
chai.expect(val).to.equal(334935275)
})
it('converts negative hex values to twos complement', () => {
const val = new Processor().hexToTwosComplement('ec094b15')
chai.expect(val).to.equal(-334935275)
})
it('converts negative 2 byte hex values to twos complement', () => {
const val = new Processor().hexToTwosComplement('ffae')
chai.expect(val).to.equal(-82)
})
</pre>
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-21222625.post-66512859993512622162019-03-27T10:40:00.000-04:002019-03-27T10:40:08.094-04:00Templated version of JavaScript Object.assignIn a <a href="https://blog.pothoven.net/2019/03/deep-copy-version-of-javascript.html">recent article</a> I provided a revised version of the JavaScript Object.assign function that also merged nested objects. In this article, I'm going to revise it slightly so that it only assigns attributes according to a template. The deepAssign method is useful for merging in partial data, some of which may be nested, into a larger set of (state) data. The templateAssign is useful for pruning the data you're going to store to only what you need / care about.<br />
<br />
<h4>
Why would you need to do this?</h4>
<br />
In <a href="https://graphql.org/">GraphQL</a> you can specify the attributes you want so probably don't need to, but in other techniques (like <a href="https://jsonapi.org/">json:api</a>) you get the full object which may contain way more data than you need, and if the data set it large it can consume a lot of memory unnecessarily. In the application this function is derived from, we have a lot of data and I've seen quite a few "Out of memory" errors in our application monitoring. So eliminating unnecessary data is valuable.<br />
<br />
The standard Object.assign (and the Object.deepAssign) method will combine all the object attributes, but if the incoming data contains extra attributes you don't care about, it can be handy to prune it to only what you care about. This templateAssign function will accept a template object as the first argument and only copy over attributes from the additional source objects that are defined in the template.<br />
<br />
<b>If this is to be used in a map-reduction system like <a href="https://redux.js.org/">Redux</a>, be sure to clone the template object into a new object via Object.assign.</b><br />
<br />
Example:<br />
Let's assume we only want attribute "a" either at the top level or a nested level
<br />
<br />
<pre>> const template = { a: undefined, deep: { a: undefined } }
> const obj1 = { a : "a", deep: { a: "a" } }
> const obj2 = { b : "b", deep: { a: "b", b: "b" } }
> const obj3 = { c : "c", deep: { b: "b", c: "c" } }
</pre>
<br />
Standard <span style="font-family: Courier New, Courier, monospace;">assign</span>, doesn't merge nested "deep" object, and keeps all (top-level) attributes
<br />
<pre>> Object.assign({}, obj1, obj2, obj3)
{ a: 'a', deep: { b: 'b', c: 'c' }, b: 'b', c: 'c' }
</pre>
<br />
<span style="font-family: Courier New, Courier, monospace;">deepAssign</span>, merges everything (top-level and nested objects)
<br />
<pre>> Object.deepAssign({}, obj1, obj2, obj3)
{ a: 'a', deep: { a: 'b', b: 'b', c: 'c' }, b: 'b', c: 'c' }
</pre>
<br />
<span style="font-family: Courier New, Courier, monospace;">templateAssign</span>, merges all levels, but only keeps attributes in the template
<br />
<pre>> Object.templateAssign(Object.assign({}, template), obj1, obj2, obj3)
{ a: 'a', deep: { a: 'b' } }
</pre>
<br />
Here's the code:<br />
<br />
<pre>if (!Object.prototype.templateAssign) {
Object.prototype.templateAssign = function(...objs) {
let target = objs.shift();
let source = objs.shift();
if (source) {
for(const attribute in source) {
if (attribute in source && typeof(source[attribute]) === "object") {
target[attribute] = Object.templateAssign(Object.assign({}, target[attribute] || {}), source[attribute]);
} else if (attribute in target &&
source.hasOwnProperty(attribute) &&
source[attribute] !== undefined) {
target[attribute] = source[attribute];
}
}
}
if (objs.length > 0) {
return Object.templateAssign(target, ...objs);
} else {
return target;
}
};
}
</pre>
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-21222625.post-56132102159738808172019-03-26T14:10:00.000-04:002019-03-26T14:10:17.190-04:00Make NVM work like RVMI've used <a href="https://rvm.io/">RVM</a> (Ruby Version Manager) for years and it has a great feature of automatically switching your Ruby version as you navigate to to project folders to use the Ruby version specified for that project. For <a href="https://github.com/creationix/nvm/blob/master/README.md">NVM</a> (Node Version Manager) you have to manually tell nvm to switch node versions via <span style="font-family: Courier New, Courier, monospace;">nvm use</span>. If you add the following code to your bash configuration, nvm will switch automatically like rvm does when it finds a .<span style="font-family: Courier New, Courier, monospace;">nvmrc</span> file. (note: I am not the original author of this code. I tweaked it from another source, but I don't recall where that was).<br />
<br />
<pre># fix NVM to work like RVM
#
find-up () {
path=$(pwd)
while [[ "$path" != "" && ! -e "$path/$1" ]]; do
path=${path%/*}
done
echo "$path"
}
cdnvm(){
cd $@;
nvm_path=$(find-up .nvmrc | tr -d '[:space:]')
# If there are no .nvmrc file, use the default nvm version
if [[ ! $nvm_path = *[^[:space:]]* ]]; then
declare default_version;
default_version=$(nvm version default);
# If there is no default version, set it to `node`
# This will use the latest version on your machine
if [[ $default_version == "N/A" ]]; then
nvm alias default node;
default_version=$(nvm version default);
fi
# If the current version is not the default version, set it to use the default version
if [[ $(nvm current) != "$default_version" ]]; then
nvm use default;
fi
elif [[ -s $nvm_path/.nvmrc && -r $nvm_path/.nvmrc ]]; then
declare nvm_version
nvm_version=$(<"$nvm_path"/.nvmrc)
# Add the `v` suffix if it does not exists in the .nvmrc file
if [[ $nvm_version != v* ]]; then
nvm_version="v""$nvm_version"
fi
# If it is not already installed, install it
if [[ $(nvm ls "$nvm_version" | tr -d '[:space:]') == "N/A" ]]; then
nvm install "$nvm_version";
fi
if [[ $(nvm current) != "$nvm_version" ]]; then
nvm use "$nvm_version";
fi
fi
}
alias cd='cdnvm'
</pre>
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-21222625.post-83881065026943666482019-03-25T13:59:00.000-04:002019-03-25T13:59:10.375-04:00Activity LEDsIn some of my applications I get ongoing data from websockets and I want some visual indication that the websocket is connected and receiving data, so I added an LED indicator much like a network router or switch would have.<br />
<br />
Whereever I want the LED, I add div to show the LED.<br />
<br />
<pre><div class="led" id="ws-led">
<div class="led-red-off">
</div>
</div>
</pre>
<br />
then add some CSS rules to make it look like an LED indicator
<br />
<br />
<pre>div.led{
display : inline-block;
vertical-align : bottom;
}
div.led-red-off,
div.led-red-on,
div.led-green-off,
div.led-green-on {
border : 0;
border-radius : 50%;
height : 1em;
width : 1em;
vertical-align : middle;
background-repeat : no-repeat;
display : inline-block;
}
div.led-red-on {
background : #F44336; opacity: 1;
}
div.led-green-on {
background : #4CAF50; opacity: 1;
}
div.led-red-off {
background : #F44336; opacity: .5;
}
div.led-green-off {
background : #4CAF50; opacity: .5;
}
</pre>
and finally, some JS code to make them blink.
<br />
<br />
<pre> var wsLED = document.getElementById('ws-led');
// connected to the Websocket server (general)
function connected(greeting) {
// change the LED from initial red to green
if (wsLED) wsLED.firstChild.setAttribute("class", "led-green-off");
}
// connection to Websocket server lost
function disconnected() {
// if we loose the websocket connection, change the LED to red
if (wsLED) wsLED.firstChild.setAttribute("class", "led-red-on");
}
/**
* logWSActivity
*
* log some WS activity
*/
function logWSActivity(...args) {
console.log(...args);
flashLED(wsLED);
}
/**
* flashLEDs
*
* The visual effect of the flashing LEDs is done by switching the
* CSS class for a 10th of a second per activity. If a new
* activity happens within that time, the timeout to turn it back
* off is reset to a new 10th of a second.
*/
var ledOffTimeout;
function flashLED(led) {
// flash activity LED
if (led) {
if (ledOffTimeout) { clearTimeout(ledOffTimeout); }
led.firstChild.setAttribute("class", "led-green-on");
ledOffTimeout = setTimeout(function() {
led.firstChild.setAttribute("class", "led-green-off"); }, 100);
}
}
</pre>
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-21222625.post-42295043586257313692019-03-25T13:26:00.001-04:002019-03-25T13:26:56.000-04:00JavaScript String functions (toCamelCase, dasherize, and titleize)I had the need for a few String functions in JavaScript to manage some variances between data sources so I figured I'd share them. For example, from one source attributes in the JSON data would use underscores to separate words and in another it would uses dashes (<span style="font-family: Courier New, Courier, monospace;">my_attribute</span> vs <span style="font-family: Courier New, Courier, monospace;">my-attribute</span>). Also for simple data display, I wanted to titleize the attribute name for a label (<span style="font-family: Courier New, Courier, monospace;">My Attribute</span>). <br />
<br />
<br />
<pre>/**
* String.toCamelCase
*
* Convert a string to camel case, including hyphens and underscores
*/
String.prototype.toCamelCase = function() {
return this.replace(/^([A-Z])|[\s-_](\w)/g, function(match, p1, p2, offset) {
if (p2) return p2.toUpperCase();
return p1.toLowerCase();
});
};
/**
* String.dasherize
*
* Dasherize a string, including periods and underscores
*/
if (!String.prototype.dasherize) {
String.prototype.dasherize = function() {
return this.replace(/^([A-Z])|[\s\._](\w)/g, function(match, p1, p2, offset) {
if (p2) return "-" + p2.toLowerCase();
return p1.toLowerCase();
});
};
}
/**
* String.capitalize
*
* Capitalize the first letter of a string
*/
if (!String.prototype.capitalize) {
String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.slice(1);
};
}
/**
* String.titleize
*
* Capitalize the first letter of every word in a string, also separates
* words by spaces if formally separated by dashes or underscores
*
*/
if (!String.prototype.titleize) {
String.prototype.titleize = function() {
return this.replace(/^([A-Z])|[\s-_](\w)/g, function(match, p1, p2, offset) {
if (p2) return " " + p2.toUpperCase();
return p1.toLowerCase();
}).capitalize();
};
}
</pre>
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-21222625.post-69908904895605016282019-03-25T13:15:00.001-04:002021-08-27T12:50:56.214-04:00Deep Copy version of Javascript Object.assignI was working on some Redux work and needed a reducer that would merge in some sparse updates to the current state of an object.<br />
<br />
If you're learning Redux you may be familiar with the tutorial example of a <a href="https://redux.js.org/basics/reducers">TODO list</a> item where it's changing one attribute of the TODO list:<br />
<br />
<pre>return Object.assign({}, state, {visibilityFilter: action.filter});
</pre>
<br />
Now, instead of changing a single top-level attribute like the visibility filter, assume you have some data that needs to be merged into the existing object both at the top level and in some nested attributes. For example, presume your current state looks like:<br />
<br />
<pre>{ a : "a", deep: { a: "a" }
</pre>
<br />
and you get new data for that state that need to be merged in that looks like:<br />
<br />
<pre>{ b : "b", deep: { b: "b" } }
</pre>
<br />
The Object.assign function will do a shallow copy merge and the result will be:<br />
<br />
<pre>> Object.assign({}, { a : "a", deep: { a: "a" } }, { b : "b", deep: { b: "b" } })
{ a: 'a', deep: { b: 'b' }, b: 'b' }</pre>
<br />
The new value of deep simply replaced the first value. The nested value of deep wasn't merged together. The method below will correctly merge nested values as follows:<br />
<br />
<pre>> deepAssign({}, { a : "a", deep: { a: "a" } }, { b : "b", deep: { b: "b" } })
{ a: 'a', deep: { a: 'a', b: 'b' }, b: 'b' }</pre>
<br />
Here's the code:
<br />
<br />
<pre>function deepAssign(...objs) {
const target = objs.shift();
const source = objs.shift();
if (source) {
if (source instanceof Array) {
for (const element of source) {
if (element instanceof Array) {
target.push(deepAssign([], element));
} else if (element instanceof Object) {
target.push(deepAssign({}, element));
} else {
target.push(element);
}
}
} else {
for (const attribute in source) {
// eslint-disable-next-line no-prototype-builtins
if (source.hasOwnProperty(attribute) && source[attribute] !== undefined) {
if (source[attribute] instanceof Array) {
target[attribute] = target[attribute] || [];
for (const element of source[attribute]) {
if (element instanceof Array) {
target[attribute].push(deepAssign([], element));
} else if (element instanceof Object) {
target[attribute].push(deepAssign({}, element));
} else {
target[attribute].push(element);
}
}
} else if (source[attribute] instanceof Object) {
if (source[attribute].toString() === '[object Object]') {
// simple data object so deep copy it
target[attribute] = deepAssign((typeof target[attribute] === 'object') ? target[attribute] : {}, source[attribute]);
} else {
// instance of some class, so just copy over the object
target[attribute] = source[attribute];
}
} else {
target[attribute] = source[attribute];
}
}
}
}
}
if (objs.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return deepAssign(target, ...objs);
} else {
return target;
}
};
</pre>
<br />
Update 7/15/19 : Original version didn't handle Arrays correctly. They become objects. They now copy correctly including nested array and object in the arrays. You can't just use the spread operator (<code>newArray = [...oldArray]</code>) to copy it or it wouldn't copy the nested objects as new objects<div><br /></div><div>Update 8/27/21 : Objects that weren't simple data objects were being lost, so now complex objects are brought over to the new copy.</div>Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-21222625.post-2770213300635012082016-04-05T18:27:00.000-04:002019-03-25T13:27:59.305-04:00Re-enable disabled disable_with fieldsI have some forms that do JavaScript based validation before submitting to the server. The submit button is using the <code>disable_with</code> construct, so when the submit button is pressed, it is correctly disabled, but when the client-side validation fails, it's stuck disabled.<br />
<br />
To remedy this situation, I added these lines to the JavaScript code that hides the error messages after they've been displayed for 5 seconds.<br />
<br />
<pre>// re-enable 'disable_with' fields after validation errors
var disabled_with = jQuery('[data-disable-with][disabled]');
if (disabled_with) {
jQuery.rails.enableElement(disabled_with);
disabled_with.val(function(){ return jQuery(this).text()}).prop('disabled', false);
}
</pre>
<br />
The first line is the commonly proposed solution that I found elsewhere online, but when I tried it (in Chrome), I found that it didn't work, leaving the button disabled with the disabled text displayed. However, one important thing it did was to insert the original button text as HTML content of the input element. I added the second line to use that text as the button value and then actually enable the button.<br />
<br />
<br />Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-21222625.post-9721241032420557372016-01-16T09:26:00.001-05:002016-01-16T09:26:39.724-05:00Setup of Ruby Development Environment VMI recently needed to provide some development environment setup instructions to a friend, so I figured it would be a good idea to document them here in case I need them again or if it helps anyone else.<br />
<br />
Let me start off with a few premises:<br />
<br />
<ul>
<li>I like doing Ruby on Rails development.</li>
<li>Ruby development on Windows is not pretty.</li>
<li>The computer I'm using most often is Windows-based (provided for my "real" job which is not Ruby development).</li>
<li>My personal computers are Macs.</li>
<li>I want to be able to work from any computer but don't want to hassle with keeping the environments in sync.</li>
<li>I've used multiple flavors of Linux over the years back to the <a href="http://en.wikipedia.org/wiki/Softlanding_Linux_System" target="_blank">SLS</a> days (Linux version 0.9x), and am very comfortable with it.</li>
</ul>
With those premises in mind, I've adopted the use of a virtual machine (VM) to be my development environment. It allows me to separate my personal development environment from other clutter (or even probing by workplace compliance scans), and allows me to readily move it from machine to machine as necessary. I also usually host my applications on Linux-based servers, so it provides me more confidence in an environment that matches the deployment environment.<br />
<br />
I personally don't like living in a VM window to work. I also feel that I'm already running a windows manager (Windows or OSX) on my host operating system, so why incur the overhead of running another one in the VM. Therefore, I run a "headless" Linux in the VM, and open X windows on the host by running a simple X server on the host OS.<br />
<br />
<h4>
Step 1 - Setup an X server on the host OS</h4>
For Windows, I recommend VcXsrv (see <a href="http://sourceforge.net/projects/vcxsrv/">http://sourceforge.net/projects/vcxsrv/</a>), but if you're already using Cygwin for other UNIX applications, the Cygwin Xserver (see
<a class="moz-txt-link-freetext" href="http://x.cygwin.com/docs/ug/setup-cygwin-x-installing.html">http://x.cygwin.com/docs/ug/setup-cygwin-x-installing.html</a>) works well, or alternatively you could try Xming
(<a class="moz-txt-link-freetext" href="http://sourceforge.net/projects/xming/files/latest/download">http://sourceforge.net/projects/xming/files/latest/download</a>).<br />
<br />
For OSX, I use XQuartz (see <a href="http://www.xquartz.org/">http://www.xquartz.org/)</a><br />
<br />
<h4>
Step 2 - Install VMWare (or VirtualBox, Parallels, etc.)</h4>
I'm not going to provide instructions for every virtualization software out there. I use VMWare so that's what I'm describing. Other solutions work just as well.<br />
<br />
For Windows, <a href="http://www.vmware.com/products/player" target="_blank">VMWare Player</a> does everything you need, and it's free!<br />
<br />
For OSX, there's no free player, so you have to purchase <a href="http://www.vmware.com/products/fusion" target="_blank">VMWare Fusion</a>. (but, hey, it's the only thing you need to buy!).<br />
<br />
<h4>
Step 3 - Download a Linux distribution ISO</h4>
Again, there are <a href="http://upload.wikimedia.org/wikipedia/commons/1/1b/Linux_Distribution_Timeline.svg" target="_blank">plenty of options</a> (Ubuntu, Red Hat, SuSE, etc), but I'm using Ubuntu here as it is frequently used for hosting. You want the <a href="http://www.ubuntu.com/download/server" target="_blank"><b>Server </b>version</a> (not Desktop) as it will be more light-weight without all the X11 + window manager overhead. Also, I recommend the LTS (longer term service) version so you don't have to deal with upgrading your OS all the time (besides security fixes of course).<br />
<br />
<h4>
Step 4 - Create a New Virtual Machine</h4>
Select the VMWare option to create a new virtual machine, point it to the ISO and it should be pretty straight forward.<br />
<br />
You don't have to be excessive with the resources you allocate for the VM. The default recommendations by VMWare will probably suffice, but if you have a little more to give, then bump it up a little. For example, the VMWare recommendation for the memory is 1GB, my system has 16GB so I gave the VM 2GB. I allowed it 4 out of the 8 processor cores, and give it up to 40GB of disk space.<br />
<br />
<h4>
Step 5 - Forward local ports to VM</h4>
While this is not strictly necessary, it is useful. <br />
<br />
<b>For Windows,</b> in earlier versions of VMWare Player, the Virtual Network Editor was included, but you needed to know the right command to access it. In case the current omission is unintentional and they add the network editor back in future releases, the method to access the network editor for VMPlayer was:<br />
<br />
<ol>
<li>Open command prompt as <b>administrator</b></li>
<li><span style="font-family: Courier New, Courier, monospace;">cd "C:\Program Files (x86)\VMware\VMware Player"</span></li>
<li><span style="font-family: Courier New, Courier, monospace;">rundll32.exe vmnetui.dll VMNetUI_ShowStandalone</span></li>
</ol>
<br />
<span style="font-size: small;">As it</span> stands currently, that will give you an error that the vmnetui.dll is not found. So instead, you need to extract the Virtual Network Editor from the VMWare Workstation package. This is done by:<br />
<br />
<ol>
<li><span style="font-family: inherit;">Download the latest VMWare Workstation package.</span></li>
<li><span style="background-color: white; font-family: inherit; font-size: 14px; line-height: 18px;">unpack it locally with this command :<br />"</span><span style="background-color: white; font-size: 14px; line-height: 18px;"><span style="font-family: Courier New, Courier, monospace;">VMWare-worksation-full-10.0.1-xxxxxx.exe /e .\ext</span></span><span style="background-color: white; font-family: inherit; font-size: 14px; line-height: 18px;">"<br />note the xxxxxx is the release id you got at download</span></li>
<li><span style="background-color: white; font-size: 14px; font-style: inherit; font-variant: inherit; line-height: inherit;"><span style="font-family: inherit;"><span style="font-weight: inherit;">Go to the newly created "</span><b>ext</b><span style="font-weight: inherit;">" directory and open the "</span><b>core.cab</b><span style="font-weight: inherit;">" (use your favorite zip util)</span></span></span></li>
<li><span style="font-family: inherit;"><span style="background-color: white; font-size: 14px; font-style: inherit; font-variant: inherit; font-weight: inherit; line-height: inherit;">G</span><span style="font-size: 14px; font-style: inherit; font-variant: inherit; line-height: inherit;"><span style="background-color: white;"><span style="font-weight: inherit;">et the "</span><b>vmnetcfg.exe</b><span style="font-weight: inherit;">" from there, and copy it to the vmwareplayer install directory</span></span></span><span style="border: 0px none; font-size: 14px; font-style: inherit; font-variant: inherit; font-weight: inherit; line-height: inherit; margin: 0px; outline: 0px; padding: 0px; vertical-align: baseline;"><span style="background-color: white;">G</span>et the "</span><span style="border: 0px none; font-size: 14px; font-style: inherit; font-variant: inherit; line-height: inherit; margin: 0px; outline: 0px; padding: 0px; vertical-align: baseline;"><b>_vmnetcfglib.dll</b></span><span style="border: 0px none; font-size: 14px; font-style: inherit; font-variant: inherit; font-weight: inherit; line-height: inherit; margin: 0px; outline: 0px; padding: 0px; vertical-align: baseline;">" file and rename it "</span><span style="background-color: white; font-size: 14px; line-height: 18px;"><b>vmnetcfglib.dll</b>", and then copy it to the same directory than for vmnetcfg.exe</span></span></li>
</ol>
<span style="font-size: 14px; line-height: 18px;"><b>OR </b><span style="font-size: small;"> </span></span><br />
<br />
<span style="font-size: 14px; line-height: 18px;"><span style="font-size: small;">Download </span></span><span style="font-size: small;"><b><a href="http://dl.pothoven.net/vmnetcfg.exe">vmnetcfg.exe</a> </b>and <b style="line-height: 18px;"><a href="http://dl.pothoven.net/vmnetcfglib.dll">vmnetcfglib.dll</a> </b><span style="line-height: 18px;">and put them in your VMWare Player install directory.</span></span><br />
<span style="font-size: small; line-height: 18px;"><br /></span>
<span style="font-size: small; line-height: 18px;">Once you have the Virtual Network Editor, select the NAT adapter (should be VMnet8), click on the "NAT Settings..." button, and add whatever ports you'll be using as shown below. In this example, my VM IP is 192.168.216.131 and I'm forwarding ports 22 for SSH and 3000 as the default Rails server port. Add port 80 if you'll be running your apps through a normal HTTP server like Apache or nginx (ex. via Passenger).</span><br />
<span style="font-size: 14px; line-height: 18px;"><br /></span>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjoUjUX7MCrRV4_ZlLL4J6xYB5QMbwZQ-YcfLe2wi-kRoo5yIw7lKWHSGc3DMmZtuKpi-BO3w0TkMhN8ILqor4gPtbQJsrO4GFUmiMDFpRV5ZuVWxZUzWLLb97P7W4e7dN8w0sY/s1600/NATFW.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="175" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjoUjUX7MCrRV4_ZlLL4J6xYB5QMbwZQ-YcfLe2wi-kRoo5yIw7lKWHSGc3DMmZtuKpi-BO3w0TkMhN8ILqor4gPtbQJsrO4GFUmiMDFpRV5ZuVWxZUzWLLb97P7W4e7dN8w0sY/s1600/NATFW.png" width="320" /></a></div>
<br /><br />
<span style="font-size: small; line-height: 18px;"><b>For Macs </b>using VMWare Fusion, edit the vmnet8 NAT config file using the terminal and your favorite editor </span><br />
<br />
<div class="crayon-pre" style="-moz-tab-size: 4; -o-tab-size: 4; -webkit-tab-size: 4; font-size: 12px !important; line-height: 15px !important; tab-size: 4;">
<div class="crayon-line" id="crayon-569938b7e6506873498652-1">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"><span class="crayon-e">sudo </span><span class="crayon-v">emacs</span><span class="crayon-h"> </span><span class="crayon-o">/</span><span class="crayon-v">Library</span><span class="crayon-o">/</span><span class="crayon-v">Preferences</span><span class="crayon-o">/</span><span class="crayon-v">VMware</span><span class="crayon-sy">\</span><span class="crayon-h"> </span><span class="crayon-v">Fusion</span><span class="crayon-o">/</span><span class="crayon-v">vmnet8</span><span class="crayon-o">/</span><span class="crayon-v">nat</span><span class="crayon-sy">.</span><span class="crayon-v">conf</span></span></span></div>
<div class="crayon-line" id="crayon-569938b7e6506873498652-1">
<div class="crayon-pre" style="-moz-tab-size: 4; line-height: 15px ! important;">
<div class="crayon-line" id="crayon-569938b7e6506873498652-1">
<br />
<span style="font-size: small;"><span class="crayon-e"><span style="font-family: Arial,Helvetica,sans-serif;">or </span></span></span></div>
<div class="crayon-line" id="crayon-569938b7e6506873498652-1">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"><span class="crayon-e"> </span></span></span></div>
<div class="crayon-line" id="crayon-569938b7e6506873498652-1">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"><span class="crayon-e">sudo </span><span class="crayon-v">vim</span><span class="crayon-h"> </span><span class="crayon-o">/</span><span class="crayon-v">Library</span><span class="crayon-o">/</span><span class="crayon-v">Preferences</span><span class="crayon-o">/</span><span class="crayon-v">VMware</span><span class="crayon-sy">\</span><span class="crayon-h"> </span><span class="crayon-v">Fusion</span><span class="crayon-o">/</span><span class="crayon-v">vmnet8</span><span class="crayon-o">/</span><span class="crayon-v">nat</span><span class="crayon-sy">.</span><span class="crayon-v">conf</span></span></span></div>
</div>
<span style="font-size: small;"><span class="crayon-v"> </span></span></div>
<div class="crayon-line" id="crayon-569938b7e6506873498652-1">
<span style="font-size: small;"><span class="crayon-v"><span style="font-family: inherit;"> </span></span></span></div>
<div class="crayon-line" id="crayon-569938b7e6506873498652-1">
<span style="font-size: small;"><span class="crayon-v"><span style="font-family: inherit;">Near the bottom you’ll see something like this</span></span></span></div>
<div class="crayon-line" id="crayon-569938b7e6506873498652-1">
<span style="font-size: small;"><span class="crayon-v"><span style="font-family: inherit;"> </span></span></span></div>
<div class="crayon-line" id="crayon-569938b7e6506873498652-1">
<div class="crayon-pre" style="-moz-tab-size: 4; line-height: 15px ! important;">
<div class="crayon-line" id="crayon-569938b7e650a795756794-1">
<span style="font-size: x-small;"><span style="font-family: "Courier New",Courier,monospace;"><span class="crayon-sy">[</span><span class="crayon-v">incomingtcp</span><span class="crayon-sy">]</span></span></span></div>
<div class="crayon-line crayon-striped-line" id="crayon-569938b7e650a795756794-2">
<span style="font-size: x-small;"><span style="font-family: "Courier New",Courier,monospace;"> </span></span></div>
<div class="crayon-line" id="crayon-569938b7e650a795756794-3">
<span style="font-size: x-small;"><span style="font-family: "Courier New",Courier,monospace;"><span class="crayon-p"># Use these with care - anyone can enter into your VM through these...</span></span></span></div>
<div class="crayon-line crayon-striped-line" id="crayon-569938b7e650a795756794-4">
<span style="font-size: x-small;"><span style="font-family: "Courier New",Courier,monospace;"><span class="crayon-p"># The format and example are as follows:</span></span></span></div>
<div class="crayon-line" id="crayon-569938b7e650a795756794-5">
<span style="font-size: x-small;"><span style="font-family: "Courier New",Courier,monospace;"><span class="crayon-p">#<external number="" port=""> = <vm address="" ip="" s="">:<vm number="" port="" s=""></vm></vm></external></span></span></span></div>
<div class="crayon-line crayon-striped-line" id="crayon-569938b7e650a795756794-6">
<span style="font-size: x-small;"><span style="font-family: "Courier New",Courier,monospace;"><span class="crayon-p">#8080 = 172.16.3.128:80</span></span></span></div>
</div>
<span style="font-size: small;"><span class="crayon-v"><span style="font-family: inherit;"> </span></span></span></div>
<div class="crayon-line" id="crayon-569938b7e6506873498652-1">
<span style="font-size: small;">This is where we’ll be putting the port forwarding. We'll assuming the same VM IP and that we still want to forward to the Rails server port 3000 and SSH port 22. Since OSX is likely already running an SSH server on port 22, we'll forward from port 2222 instead. So, add the lines:</span><br />
<br />
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;">3000 = 192.168.216.131:3000</span></span><br />
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;">2222 = 192.168.216.131:22 </span></span><br />
<br />
<br />
<span style="font-size: small;"><span style="font-family: inherit;">Restart VMWare Fusion and it should be forwarding to these ports for you. Alternatively, you could restart the VMWare networking using the command line:</span></span><br />
<span style="font-size: small;"><span style="font-family: inherit;"><br /></span></span>
<span style="font-size: x-small;"><span style="font-family: "Courier New",Courier,monospace;"><br /></span></span>
<div class="crayon-pre" style="-moz-tab-size: 4; line-height: 15px ! important;">
<div class="crayon-line" id="crayon-569938b7e64e1569398502-1">
<span style="font-size: x-small;"><span style="font-family: "Courier New",Courier,monospace;"><span class="crayon-v">sudo</span><span class="crayon-h"> </span><span class="crayon-o">/</span><span class="crayon-v">Applications</span><span class="crayon-o">/</span><span class="crayon-v">VMware</span><span class="crayon-sy">\</span><span class="crayon-h"> </span><span class="crayon-v">Fusion</span><span class="crayon-sy">.</span><span class="crayon-v">app</span><span class="crayon-o">/</span><span class="crayon-v">Contents</span><span class="crayon-o">/</span><span class="crayon-v">Library</span><span class="crayon-o">/</span><span class="crayon-v">vmnet</span><span class="crayon-o">-</span><span class="crayon-v">cli</span><span class="crayon-h"> </span><span class="crayon-o">--</span><span class="crayon-e">stop</span></span></span></div>
<div class="crayon-line crayon-striped-line" id="crayon-569938b7e64e1569398502-2">
<span style="font-size: x-small;"><span style="font-family: "Courier New",Courier,monospace;"><span class="crayon-v">sudo</span><span class="crayon-h"> </span><span class="crayon-o">/</span><span class="crayon-v">Applications</span><span class="crayon-o">/</span><span class="crayon-v">VMware</span><span class="crayon-sy">\</span><span class="crayon-h"> </span><span class="crayon-v">Fusion</span><span class="crayon-sy">.</span><span class="crayon-v">app</span><span class="crayon-o">/</span><span class="crayon-v">Contents</span><span class="crayon-o">/</span><span class="crayon-v">Library</span><span class="crayon-o">/</span><span class="crayon-v">vmnet</span><span class="crayon-o">-</span><span class="crayon-v">cli</span><span class="crayon-h"> </span><span class="crayon-o">--</span><span class="crayon-v">start</span></span></span></div>
</div>
<span style="font-size: small;"><span style="font-family: inherit;"><br /></span></span>
<span style="font-size: small;"><span style="font-family: inherit;"><br /></span></span>
<span style="font-size: small;"><span style="font-family: inherit;">Optionally, if you want to ensure your VM always gets the same IP address, you can edit </span><span style="font-family: "Courier New",Courier,monospace;"><span class="crayon-o">/</span><span class="crayon-v">Library</span><span class="crayon-o">/</span><span class="crayon-v">Preferences</span><span class="crayon-o">/</span><span class="crayon-v">VMware</span><span class="crayon-sy">\</span><span class="crayon-h"> </span><span class="crayon-v">Fusion</span><span class="crayon-o">/</span><span class="crayon-v">vmnet8</span><span class="crayon-o">/</span><span class="crayon-v">dhcpd</span><span class="crayon-sy">.</span><span class="crayon-v">conf</span></span><span style="font-family: inherit;"> and add a clause to the bottom with the format of:</span></span><br />
<span style="font-size: small;"><span style="font-family: inherit;"><br /></span></span>
<span style="font-size: small;"><br /></span>
<div class="crayon-pre" style="-moz-tab-size: 4; line-height: 15px ! important;">
<div class="crayon-line" id="crayon-569938b7e64fc615910054-1">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"><span class="crayon-e">host</span><span class="crayon-h"> </span><span class="crayon-o"><</span><span class="crayon-e">some</span><span class="crayon-o">-</span><span class="crayon-e">name</span><span class="crayon-o">></span><span class="crayon-h"> </span><span class="crayon-sy">{</span></span></span></div>
<div class="crayon-line crayon-striped-line" id="crayon-569938b7e64fc615910054-2">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"><span class="crayon-e"><span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"> </span></span>hardware </span><span class="crayon-v">ethernet</span><span class="crayon-h"> </span><span class="crayon-o"><</span><span class="crayon-v">MAC</span><span class="crayon-o">-</span><span class="crayon-v">ADDRESS</span><span class="crayon-o">></span><span class="crayon-sy">;</span></span></span></div>
<div class="crayon-line" id="crayon-569938b7e64fc615910054-3">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"><span class="crayon-v"><span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"> </span></span>fixed</span><span class="crayon-o">-</span><span class="crayon-v">address</span><span class="crayon-h"> </span><span class="crayon-o"><</span><span class="crayon-v">IP</span><span class="crayon-o">-</span><span class="crayon-v">address</span><span class="crayon-o">></span><span class="crayon-sy">;</span></span></span></div>
<div class="crayon-line crayon-striped-line" id="crayon-569938b7e64fc615910054-4">
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;"><span class="crayon-sy">}</span></span></span></div>
</div>
<span style="font-size: small;"><span style="font-family: inherit;"><br /></span></span>
<span style="font-size: small;"><span style="font-family: inherit;">You can determine the VMWare assigned MAC address for your VM by looking in your VM's *.vmx file and look for a line like:</span></span><br />
<span style="font-size: small;"><span style="font-family: inherit;"><br /></span></span>
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;">ethernet0.generatedAddress = "00:0c:29:42:f0:54"</span></span><br />
<span style="font-size: small;"><span style="font-family: inherit;"><br /></span></span>
<span style="font-size: small;"><span style="font-family: inherit;">Then to assign a static IP address of 192.168.216.131 to this VM, you could add the clause:</span></span><br />
<span style="font-size: small;"><span style="font-family: inherit;"><br /></span></span>
<span style="font-size: small;"><span style="font-family: "Courier New",Courier,monospace;">host ubuntu {<br /> hardware ethernet 00:0c:29:42:f0:54;<br /> fixed-address </span><span style="font-family: inherit;"><span style="font-family: "Courier New",Courier,monospace;">192.168.216.131;<br />}</span></span></span><br />
<br />
<span style="font-size: small;"><span style="font-family: inherit;">Remember to restart the Fusion Networking services either by restarting Fusion or using the command line option.</span></span><br />
</div>
</div>
Most of the OSX instructions came from <a href="http://encyclopediaofdaniel.com/blog/fusion-dhcp-port-forwarding/">http://encyclopediaofdaniel.com/blog/fusion-dhcp-port-forwarding/</a> where you can also find information on a Ruby gem to make the changes for you.<br />
<br />
<h4>
Step 6 - Connect using SSH with X forwarding</h4>
<span style="font-size: small;"><span style="font-family: inherit;"></span></span><div class="post-text" itemprop="text">
<span style="font-size: small;"><span style="font-family: inherit;">
</span></span><span style="font-size: small;"><span style="font-family: inherit;">X11 forwarding needs to be enabled on both the client side and the server side.</span></span><br />
<br />
<span style="font-size: small;"><span style="font-family: inherit;">
</span></span><span style="font-size: small;"><span style="font-family: inherit;">On the client side, the <code>-X</code> (capital X) option to <code>ssh</code> enables X11 forwarding, and you can make this the default (for all connections or for a specific conection) with <code>ForwardX11 yes</code> in <a href="http://www.openbsd.org/cgi-bin/man.cgi?query=ssh_config&sektion=5"><code>~/.ssh/config</code></a>.</span></span><br />
<br />
<span style="font-size: small;"><span style="font-family: inherit;">
</span></span><span style="font-size: small;"><span style="font-family: inherit;">On the server side, <code>X11Forwarding yes</code> must specified in <a href="http://www.openbsd.org/cgi-bin/man.cgi?query=sshd_config&sektion=5"><code>/etc/ssh/sshd_config</code></a>. Note that the default is no forwarding (some distributions turn it on in their default <code>/etc/ssh/sshd_config</code>), and that the user cannot override this setting.</span></span><br />
<br />
<span style="font-size: small;"><span style="font-family: inherit;">
</span></span><span style="font-size: small;"><span style="font-family: inherit;">The <code>xauth</code> program must be installed on the server side. If there are any X11 programs there, it's very likely that <code>xauth</code> will be there. In the unlikely case <code>xauth</code> was installed in a nonstandard location, it can be called through <a href="http://www.openbsd.org/cgi-bin/man.cgi?query=sshd&sektion=8#SSHRC"><code>~/.ssh/rc</code></a> (on the server!).</span></span><br />
<br />
<span style="font-size: small;"><span style="font-family: inherit;">
</span></span><span style="font-size: small;"><span style="font-family: inherit;">Note that you do not need to set any environment variables on the server. <code>DISPLAY</code> and <code>XAUTHORITY</code> will automatically be set to their proper values. If you run ssh and <code>DISPLAY</code> is not set, it means ssh is not forwarding the X11 connection.</span></span><br />
<br />
<span style="font-size: small;"><span style="font-family: inherit;">
</span></span><span style="font-size: small;"><span style="font-family: inherit;">To confirm that ssh is forwarding X11, check for a line containing <code>Requesting X11 forwarding</code> in the <code>ssh -v -X</code> output. Note that the server won't reply either way.</span></span><br />
<br />
<span style="font-size: small;"><span style="font-family: inherit;">(credit to <a href="http://unix.stackexchange.com/questions/12755/how-to-forward-x-over-ssh-from-ubuntu-machine">http://unix.stackexchange.com/questions/12755/how-to-forward-x-over-ssh-from-ubuntu-machine</a>) </span></span><br />
</div>
Unknownnoreply@blogger.com1tag:blogger.com,1999:blog-21222625.post-24640951356283946692015-11-20T14:50:00.000-05:002015-11-20T14:50:17.926-05:00Updated Clip All function for Publix Digital CouponsOver a year ago, I provided the <a href="http://blog.pothoven.net/2014/01/clip-all-function-for-publix-digital.html" target="_blank">Clip All function for Publix Digital Coupons</a>. It's been working well and I've received plenty of positive feedback for it. <br />
<br />
This month, Publix revised their <a href="https://www.publix.com/savings/coupons/digital-coupons" target="_blank">digital coupon</a> site and the function stopped working. I have fixed my code and it should work again! There are a few things to point out though.<br />
<br />
First, the method I used in the past for attaching my JavaScript code to their site no longer works. When I do that, it causes the page to re-load itself, which then negates the addition of my code. I now load the code and invoke the function from within the bookmarklet. That works out fine, but that method no longer allows me to re-direct you to the coupon site if you're not currently there. So you need to invoke the code from the coupon site.<br />
<br />
Second, the no longer have links to more pages of coupons, but load more dynamically as you scroll down the page. I try to make the code emulate the scrolling activity to automatically load more coupons, but it doesn't always trigger the event to load more coupons. So, if you're sitting on the bottom of the page with all the coupons 'clipped', try to scroll up and down a little to see if more coupons start loading. If more load, the auto-clipping function should kick back into action.<br />
<br />
<b>Please go to <a href="https://pothoven.net/PublixClipper.html">https://pothoven.net/PublixClipper.html</a> for installation instructions</b>, and please drop me a comment here if you try it out to let me know how it works for you.Unknownnoreply@blogger.com14tag:blogger.com,1999:blog-21222625.post-53226266283463226422014-01-07T16:06:00.001-05:002014-01-07T16:44:52.444-05:00Bishop Swap Puzzle Fixed!I implemented a <a href="http://pothoven.net/bishopPuzzle.html" target="_blank">Bishop Swap Puzzle</a> back in 2007 (see <a href="http://blog.pothoven.net/2007/10/bishop-swap-puzzle.html" target="_blank">my prior post</a>), but it has had a bug in it where bishops of the same color could pass through each other. I had noted it as a '<span style="font-family: Courier New, Courier, monospace;">TODO</span>' in my code comments while I was writing it, but I had neglected to go back and fix it and forgot all about it until I was trying it out again recently.<br />
<br />
I'm happy to say that I've now fixed the bug and it works correctly, so happy gaming!<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://pothoven.net/bishopPuzzle.html" target="_blank"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMc8__Owsiy_-2sjra1v7mYTIqURNI83maON4Xz8vOQ6Ss2Olq9VYg1k0QG7YZ7ojmjUJNzgSCcDhcvyU7unrh0QsOQloF4cj-8jvCQuXnhsfRYYzCpFTSCCNntKutpWeNLMd0/s1600/Screenshot-Bishop+Swap+Puzzle.png" height="238" width="400" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
Once you've completed the Bishop Swap puzzle, be sure to check out the others!</div>
<div class="separator" style="clear: both; text-align: left;">
</div>
<ul>
<li><a href="http://pothoven.net/queenPuzzle.html" target="_blank">Eight Queens Puzzle<br /><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGcr9sBC6BfwZZ1QJ2YJ_ecJvavrdqkqR2OdaWLRcT3SEi1L4aZFtVTDdRsGdE-5H0XdXRigOzp4c5qrAbzV-rBfXWJeS1Q3JgIOcwsyFtwdIRFVHi-VIsaYF-JdPa83xZVDdI/s1600/Screenshot-Eight+Queens+Puzzle.png" height="122" width="200" /></a>
</li>
<li><a href="http://pothoven.net/knightPuzzle.html" target="_blank">Knight Swap Puzzle<br /><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhgsPvLw_C3v-TYOTS-xsG5-UgLUWIXzziC7Ep3Ro40NE5i4_vVpFeXm4QSPFilVUxtSIfJpuNmS5X2N9eSFGKRW26fuys2PtsN7iytxRbiaFlbDIyFAieygantpJTGvKdZEK4-/s1600/Screenshot-Knight+Swap+Puzzle.png" height="118" width="200" /></a></li>
<li><a href="http://pothoven.net/knightPuzzle2.html" target="_blank">Knight Swap Puzzle 2<br /><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjc01mwDLnCDD3kKLu18Z6uBFNW6KTzh7xn3ec-s3qgtzogNlSahU_2xqUdMDKu3AauqK6unaHPUN7CPKTdhme3mAKnhgcYBTdGcfnvJZuTAtcKoFxdp44Xtse6VYUBatSaYwNJ/s1600/knightSwap2.png" height="121" width="200" /></a></li>
</ul>
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-21222625.post-30415229883990484202014-01-02T17:01:00.002-05:002020-12-18T10:13:27.110-05:00Clip All function for Publix Digital Coupons<span style="font-size: x-large;">Happy New Year!</span><br />
<span style="font-size: x-large;"><br /></span>
Sadly, it's been over a year since my last post. To make up for not contributing for so long, I'll provide a little gift for those of you who have resolved to save more in the new year. In particular, you need to live in the southeastern United States and do your grocery shopping at <a href="http://www.publix.com/" rel="nofollow" target="_blank">Publix</a>. <br />
<br />
Last year, Publix introduced a digital coupon site. This site provides a collection of coupons that you can "clip" and then when you're at the checkout lane, you enter your phone number on the credit card reader keypad and it will automatically apply any coupons you have clipped that apply to the groceries you have purchased. The problem with the site is that they have not provided a "Clip All" function, so you need to look through several pages of coupons and clip any coupon you're interested in one at a time. This can be a very time consuming process as many pages of coupons can be added each week.<br />
<br />
To rectify this problem, I created a bookmarklet that provides a "Clip All" function. It will clip all on the current page of coupons and continue to navigate through all available pages of coupons and clip them all. The bookmarklet also works from mobile devices (at least iPads) so you can even quickly clip all the digital coupons while you're in Publix using their WiFi.<br />
<br />
<b>Please go to <a href="https://pothoven.net/PublixClipper.html">https://pothoven.net/PublixClipper.html</a> for installation instructions</b>, and please drop me a comment here if you try it out to let me know how it works for you.<br />
<br />
<br />Unknownnoreply@blogger.com33tag:blogger.com,1999:blog-21222625.post-85192552773699458202012-10-29T16:07:00.000-04:002012-10-29T16:07:48.416-04:00Ruby 1.8.7 vs 1.9.3 performanceThere are plenty of Ruby 1.8 to 1.9 benchmark results out there, and this is by no means as thorough as most. I thought I'd share the results for one of my websites when I upgraded it from Ruby 1.8.7 to Ruby 1.9.3 as demonstrated in my New Relic report for the site. Conveniently, I upgraded the site on a Monday morning, and since that's also
when New Relic reports switch over, it provided a fairly clean week-to-week comparison (perhaps 1/3 of Monday 10/22 was still Ruby 1.8.7).<br />
<br />
<h3>
High Level Summary </h3>
<div class="separator" style="clear: both; text-align: center;">
</div>
With no other changes, other than fixes required to allow the application to work with Ruby 1.9.3, simply upgrading Ruby resulted in the overall response time average for the week dropped from 304ms to 72ms (a <b>76.32% drop</b> in response time).<br />
<br />
Unfortunately, the week immediately before the upgrade was a bit of an anomaly. The week before that had a 153ms average response time, which is more in line with typical weekly results. However, that's still a <b>53% reduction</b> in response time from 1.8.7 to 1.9.3.<br />
<br />
Here's a daily comparison for the 3 weeks: <br />
<br />
<h3>
Ruby 1.8.7 </h3>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj_7tNdbIV0Dhyphenhyphenc9YXEX6mFNeSvRdnYZEsWI2bN-LKCfPsSm61o1S9cvZ1oGsijQzW0FJsD9AByaqYOY8A1G52Yj29CDw-YsWWP7GI9wt-aBeKn0hQ-pc-D2CzaQY66pygmXjnb/s1600/Screen+Shot+2012-10-29+at+3.58.13+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj_7tNdbIV0Dhyphenhyphenc9YXEX6mFNeSvRdnYZEsWI2bN-LKCfPsSm61o1S9cvZ1oGsijQzW0FJsD9AByaqYOY8A1G52Yj29CDw-YsWWP7GI9wt-aBeKn0hQ-pc-D2CzaQY66pygmXjnb/s1600/Screen+Shot+2012-10-29+at+3.58.13+PM.png" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBMGhyaReiFseDsrkAFF9CelF8N1rPKzcIrF-1agPQCJu9RkVA4lE_gqOWVH-AxEbB6iZqyOpc1MrdVH5VnkMj8CC98Ho0b0bHZmI5MnbsJwPlzKJ7uUEjPhY7DSBNohJLDXGB/s1600/Screen+Shot+2012-10-29+at+3.44.56+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBMGhyaReiFseDsrkAFF9CelF8N1rPKzcIrF-1agPQCJu9RkVA4lE_gqOWVH-AxEbB6iZqyOpc1MrdVH5VnkMj8CC98Ho0b0bHZmI5MnbsJwPlzKJ7uUEjPhY7DSBNohJLDXGB/s1600/Screen+Shot+2012-10-29+at+3.44.56+PM.png" /></a></div>
<br />
<h3>
Ruby 1.9.3</h3>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg9x5G-gjg7EzXCq7WTWmd4J5hzcK_M0SSSthH-d3OCUbhHpWvoe8q8XbxiKvXkAR3bsOVMBhtCNArdzReNYzUQuT8JU2HYlIGaN_R2FluMEHIeILjA7qLzirwkE_7vbbkhvyeR/s1600/Screen+Shot+2012-10-29+at+3.45.24+PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg9x5G-gjg7EzXCq7WTWmd4J5hzcK_M0SSSthH-d3OCUbhHpWvoe8q8XbxiKvXkAR3bsOVMBhtCNArdzReNYzUQuT8JU2HYlIGaN_R2FluMEHIeILjA7qLzirwkE_7vbbkhvyeR/s1600/Screen+Shot+2012-10-29+at+3.45.24+PM.png" /></a></div>
<br />
<br />
Keep up the good work, Ruby development team!Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-21222625.post-43466861027342993992012-10-29T11:55:00.001-04:002012-10-29T11:56:11.107-04:00attachment_fu as a gem for Rails 3.2As I've mentioned <a href="http://blog.pothoven.net/2012/05/st-francis-society-animal-rescue.html" target="_blank">before</a>, I develop the web site for <a href="http://stfrancisrescue.org/" target="_blank">St. Francis Society Animal Rescue</a>. As I've also described in that earlier article, it started as a Rails 1.2 app, then Rails 2.1, then 2.3. Right now it's in Rails 3.1, but I'm about to switch it to Rails 3.2. Having some history, it was developed to use <a href="https://github.com/technoweenie/attachment_fu" target="_blank">attachment_fu</a> to upload all the images for the cats and dogs. While I know many people have abandoned attachment_fu for paperclip, or carrierwave, or dragonfly, etc. attachment_fu has continued to work just fine for me so <i>if it ain't broke, don't fix it</i>. Plus, there are currently over 13,000 animal images that have been uploaded, and I don't really want to hassle with converting them over to a new attachment system.<br />
<br />
<h3>
Enter Ruby 1.9.x</h3>
The last official update to the attachment_fu github repository was on <a href="https://github.com/technoweenie/attachment_fu/commits/master" target="_blank">April 25, 2009</a>. While that update is for Ruby 1.9 compatibility fixes, if you try to use it as is for Ruby 1.9.3, you'll find it won't work. To that end, I've forked off a new repository that will fix additional Ruby 1.9.3 incompatibilities. If you want to continue to use attachment_fu as a plugin with Ruby 1.9.x and Rails prior to 3.2, you can use <a href="https://github.com/pothoven/attachment_fu">my github repository</a> in your project simply by issuing this command in your project:<br />
<br />
<pre class="code">git submodule add https://github.com/pothoven/attachment_fu.git vendor/plugins/attachment_fu
</pre>
<br />
<h3>
Enter Rails 3.2.x</h3>
attachment_fu has always functioned as a plugin (vendor/plugins), the problem is that Rails 3.2 will give you this error if you continue to use it as a plugin:<br />
<br />
<pre>DEPRECATION WARNING: You have Rails 2.3-style plugins in vendor/plugins! Support for these plugins will be removed in Rails 4.0.
Move them out and bundle them in your Gemfile, or fold them in to your app as lib/myplugin/* and config/initializers/myplugin.rb.
See the release notes for more on this: http://weblog.rubyonrails.org/2012/1/4/rails-3-2-0-rc2-has-been-released.
</pre>
<br />
To that end, I've also updated my fork of attachment_fu to function as a gem! Simply add this line to your <code>Gemfile</code><br />
<code> </code>
<br />
<pre class="code">gem 'pothoven-attachment_fu'
</pre>
<br />
I need to acknowledge <a href="https://github.com/tdd">Christophe Porteneuve</a> for doing most of the gem conversion work. I just pulled in his updates and fixed a few problems with it that I encountered. Please feel free to let me know if you have any problems using the gem.
<br />Unknownnoreply@blogger.com7tag:blogger.com,1999:blog-21222625.post-603035764939052012-10-04T14:14:00.001-04:002014-07-28T15:26:05.615-04:00Self-marking required fields in RailsDavid Sulc wrote a nice article entitled, <a href="http://davidsulc.com/blog/2011/05/01/self-marking-required-fields-in-rails-3/" rel="nofollow" target="_blank">Self-marking required fields in Rails 3</a> that utilized a key element of Ryan Bates' Railscast on <a href="http://railscasts.com/episodes/211-validations-in-rails-3" rel="nofollow" target="_blank">Validations in Rails 3</a>. In Ryan's original solution, he creates a new helper method named <code>mark_required</code> that uses the Rails validation reflection to determine if the model requires that the field to be present (via <code>validates :presence => true</code> or <code>validates_presence_of</code>). If the field is required, this helper method adds a '*', otherwise it does nothing. Using this helper method in your code, your code would look like:<br />
<pre class="code"><%= f.label :name %><%= mark_required(@user, :name) %>
<%= f.text_field :name %>
</pre>
<br />
David improves upon this idea by enhancing the <code>ActionView::Helpers::FormBuilder</code> <a href="http://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-label" rel="nofollow" target="_blank">label</a> helper instead of adding a new helper. This eliminates the need to add the extra call to the <code>mark_required</code> helper for every label, which can be easily forgotten. If the field is required, the enhanced <code>label</code> helper appends a "*" to the label text to indicate that the field is required. So now your code is back to the normal form of:<br />
<pre class="code"><%= f.label :name %>
<%= f.text_field :name %>
</pre>
<br />
That's a nice improvement, but I thought it would be an even better design practice to simply give the label a <code>required</code> CSS class, and then allow the designer to be able to easily modify how required fields should be indicated. My version of <code>config/initializers/form_builder.rb</code> looks like:<br />
<br />
<pre class="code">class ActionView::Helpers::FormBuilder
alias :orig_label :label
# add a 'required' CSS class to the field label if the field is required
def label(method, content_or_options = nil, options = nil, &block)
if content_or_options && content_or_options.class == Hash
options = content_or_options
else
content = content_or_options
end
if object.class.validators_on(method).map(&:class).include? ActiveModel::Validations::PresenceValidator
if options.class != Hash
options = {:class => "required"}
else
options[:class] = ((options[:class] || "") + " required").split(" ").uniq.join(" ")
end
end
self.orig_label(method, content, options || {}, &block)
end
end
</pre>
With this revised version in place, all labels of required fields are given a <code>required</code> CSS class. If you want to indicate required fields with an asterisk after the label, you can do that by adding this CSS rule to your stylesheet,<br />
<br />
<pre class="code">/* add required field asterisk */
label.required:after {
content: " *";
}
</pre>
However, by extracting the method of indicating that the field is required from being hard-coded in the helper to instead using a CSS class, anything you can do with CSS is open to you and can be quickly and easily modified without updating the helper code. You can change colors, fonts, borders, add images before or after, you could even use the dreaded <code>text-decoration:blink</code>. The options are nearly endless.<br />
<br />
<h3>
<b>Update for Rails 4!</b> </h3>
<br />
Thanks to the input from <a href="http://www.blogger.com/profile/15727706088593642812" target="_blank">alex_m</a> and <a href="http://www.blogger.com/profile/11056186167237314184" target="_blank">Dan</a> in the comments below, the follow revised version should work for Rails 4 (with ActiveAdmin):<br />
<br />
<pre class="code">class ActionView::Helpers::FormBuilder
alias :orig_label :label
# add a 'required' CSS class to the field label if the field is required
def label(method, content_or_options = nil, options = nil, &block)
if content_or_options && content_or_options.class == Hash
options = content_or_options
else
content = content_or_options
end
if object.class.respond_to?(:validators_on) &&
object.class.validators_on(method).map(&:class).include?(ActiveRecord::Validations::PresenceValidator)
if options.class != Hash
options = {:class => "required"}
else
options[:class] = ((options[:class] || "") + " required").split(" ").uniq.join(" ")
end
end
self.orig_label(method, content, options || {}, &block)
end
end</pre>
<br />
Thanks again <a href="http://www.blogger.com/profile/15727706088593642812" target="_blank">alex_m</a> and <a href="http://www.blogger.com/profile/11056186167237314184" target="_blank">Dan</a>!Unknownnoreply@blogger.com6tag:blogger.com,1999:blog-21222625.post-72508377061348452022012-05-10T18:04:00.002-04:002020-12-19T11:19:35.735-05:00Rails Rumble 2010<div class="separator" style="clear: both; text-align: center;">
<a href="#" style="clear: right; float: right; margin-bottom: 1em; margin-left: 1em;"><img border="0" src="http://www.stevenpothoven.com/assets/railsrumble_commendablekids.png" /></a></div>
In my long absence from posting, I see I totally skipped 2010.<br />
For the <a href="http://archive.railsrumble.com/entries?utf8=%E2%9C%93&q=Commendable+Kids&year=2010&country=&award=&commit=Filter">2010 Rails Rumble</a> we created Commendable Kids.<br />
<br />
Here are the results for our entry CommendableKids.com:<br />
<ul class="rr-results" style="margin: 20px auto; width: 400px;">
<li>
Finished 4th place out of 180 teams
</li>
<li>
Winners of the Appearance Category<br />
</li>
<li>
Runners Up from Chargify as Most Potential to Monetize<br />
<a href="http://blog.railsrumble.com/blog/2010/10/28/02-chargify-challenge-winner" target="_new">Read More</a>
</li>
<li>
#1 team from the U.S.<br />
</li>
<li>
Tech Crunch Top 5<br />
<a href="http://techcrunch.com/2010/10/22/rails-rumble-2010/" target="_new">Read the Review</a>
</li>
</ul>Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-21222625.post-24861320436711901422012-05-10T17:13:00.002-04:002020-12-18T10:15:15.868-05:00St. Francis Society Animal Rescue<div class="separator" style="clear: both; text-align: center;">
<a href="http://www.stfrancisrescue.org/assets/stfrancis/logo_white.png" style="background-color: #1d75bc; clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" src="http://www.stfrancisrescue.org/assets/stfrancis/logo_white.png" /></a></div>
One of my side-projects is the development of the <a href="http://www.stfrancisrescue.org/">St. Francis Society Animal Rescue</a> web site. My friend <a href="http://brianburridge.com/">Brian Burridge</a> and I originally converted the site from a static web site to a Ruby on Rails site several years ago (2008 timeframe). I believe it was initially a Rails 1.2 project. Besides the public site, it includes a fairly involved back-end administration component that beyond just allowing content management of the web site, performs all the animal rescue administration functions (detailed animal information with health records, adoption records, etc.). At the time we were both fairly new to Ruby on Rails and we decided to use <a href="https://github.com/activescaffold/active_scaffold">ActiveScaffold</a> to build this administration component.<br />
<br />
At some point we upgraded to Rails 2.1 and then in 2010, Brian left the project to be able to better focus on his other numerous projects and I further upgraded it to Rails 2.3.x. These Rails version upgrades were more effort than a typical Rails upgrades may be due to various gem dependencies, most specifically ActiveScaffold. ActiveScaffold is really a pretty nice framework for admin sites, but it had limitations that required work-arounds and those work-arounds often didn't work when upgrading.<br />
<br />
During the end of 2011 to the beginning of 2012, I did a more drastic upgrade. I migrated to Rails 3.1.x. However, this wasn't just a simple migration, I decided to re-write the entire application. The most involved part of this re-write is what most people will never see, the administration area. I decided to totally abandon ActiveScaffold. Brian told me about <a href="http://activeadmin.info/">ActiveAdmin</a> that he was using on some of his other projects, but after fighting with ActiveScaffold for long, I opted to stay clear of such a major framework dependency and just wrote the entire admin from scratch with straight Rails.<br />
<br />
<h4>
Comparisons of the old and new administration pages.</h4>
Here's a comparison of the listing of the cats. I reduced the amount of information displayed on the list to reduce some clutter, and I've made some of the frequently changed values available to be changed directly on the list (save instantly via Ajax) to eliminate the need to open the edit form just to change a status.<br />
<br />
<div style="width: 100%;">
<div style="float: left; width: 49%;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZ560BmPOvu1arIUHp18uXL_wr3ZM2XS7QrlsTJkxq36itElUPf7NiYKyBjc3pMPRhZAypnn5XNB9heDPYMBjX2LjK0-eLj9oUjcPS6XPihPGWOlsmOuEZFgn3lWGeDNXUkkJk/s1600/OLD_st_francis_society_admin_cat_list.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZ560BmPOvu1arIUHp18uXL_wr3ZM2XS7QrlsTJkxq36itElUPf7NiYKyBjc3pMPRhZAypnn5XNB9heDPYMBjX2LjK0-eLj9oUjcPS6XPihPGWOlsmOuEZFgn3lWGeDNXUkkJk/s320/OLD_st_francis_society_admin_cat_list.png" width="100%" /></a></div>
<div style="float: right; width: 49%;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEipkO1_FvcZXfnaHI2H_N21AeGSU2_HFcl_ehQjpbfq5sFRbD4rKHlBcum80_NB3Z2j9i8rStmht3EX4VjQ5-gWh8sQmgtOFqAD_OfreqT02bVonaoI3mZcY3ITnck6R3XdAAOE/s1600/Screen+Shot+2012-05-10+at+4.05.04+PM.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEipkO1_FvcZXfnaHI2H_N21AeGSU2_HFcl_ehQjpbfq5sFRbD4rKHlBcum80_NB3Z2j9i8rStmht3EX4VjQ5-gWh8sQmgtOFqAD_OfreqT02bVonaoI3mZcY3ITnck6R3XdAAOE/s320/Screen+Shot+2012-05-10+at+4.05.04+PM.png" width="100%" /></a></div>
</div>
<div style="clear: both;">
<br /></div>
Filtering the list required an extension to ActiveScaffold that was problematic to upgrade, and it resulted in a very large area added to the top of the list. Now it uses a jQuery UI dialog.<br />
<br />
<div style="width: 100%;">
<div style="float: left; width: 49%;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhTrato5OVYDYSGCaJz4w7Kk9wWvIMsma5SOC61j7HQb8ob9dHg0grlH4JcW1InRyMsKWc6DbykQmycFJFiwCxULhJRdhat-w6Jer4juKszTAJjqN2mDuSmGELdWzEShE9Ytt0J/s1600/OLD_st_francis_society_admin_cat_filter.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhTrato5OVYDYSGCaJz4w7Kk9wWvIMsma5SOC61j7HQb8ob9dHg0grlH4JcW1InRyMsKWc6DbykQmycFJFiwCxULhJRdhat-w6Jer4juKszTAJjqN2mDuSmGELdWzEShE9Ytt0J/s320/OLD_st_francis_society_admin_cat_filter.png" width="100%" /></a></div>
<div style="float: right; width: 49%;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXzPx-NI5seWnVvflUHxVBs9V6UHbS-1oDMn-i1nNmNb7C00GpacxiVVQlsAMbdimDUTLow98cB-kxmj9k4xdBNuLJND9dxOVDXSPf5kPZap8GFWhfDYWguMaZBO0Wvc0TlIZs/s1600/Screen+Shot+2012-05-10+at+4.20.59+PM.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXzPx-NI5seWnVvflUHxVBs9V6UHbS-1oDMn-i1nNmNb7C00GpacxiVVQlsAMbdimDUTLow98cB-kxmj9k4xdBNuLJND9dxOVDXSPf5kPZap8GFWhfDYWguMaZBO0Wvc0TlIZs/s320/Screen+Shot+2012-05-10+at+4.20.59+PM.png" width="100%" /></a></div>
</div>
<div style="clear: both;">
<br /></div>
ActiveScaffold constructed its forms very vertically which didn't utilize the space well. I now have full control of the layout allowing me to organize things better.<br />
<br />
<div style="width: 100%;">
<div style="float: left; width: 49%;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhiJhP9ZDZFgIONSXS6tGMVqyA3sD-zmTenQYUiPOIX-n5m-lchlTJVDbUOKYLEISxR2wqh_zbdUV3Ylakg8Z0XuqqKQa7RgTRtX__ib_1CJStbWyuiRTP1r2dVA16aybxZNRaC/s1600/OLD_st_francis_society_admin_cat_health.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhiJhP9ZDZFgIONSXS6tGMVqyA3sD-zmTenQYUiPOIX-n5m-lchlTJVDbUOKYLEISxR2wqh_zbdUV3Ylakg8Z0XuqqKQa7RgTRtX__ib_1CJStbWyuiRTP1r2dVA16aybxZNRaC/s320/OLD_st_francis_society_admin_cat_health.png" width="100%" /></a></div>
<div style="float: right; width: 49%;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh0aKibJSPVrBHPPI6526kgGh5J0ky6dZc_eo5y0VDSxzIcbTYEBx9kuO4JNIFlkM7fEDvn8lynYUuk_YohGNWnwTMV-tgOSpAfqHIAJIlswejWXVz5EHdrL5q2iBL-RbvK2V5l/s1600/Screen+Shot+2012-05-10+at+4.29.42+PM.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh0aKibJSPVrBHPPI6526kgGh5J0ky6dZc_eo5y0VDSxzIcbTYEBx9kuO4JNIFlkM7fEDvn8lynYUuk_YohGNWnwTMV-tgOSpAfqHIAJIlswejWXVz5EHdrL5q2iBL-RbvK2V5l/s320/Screen+Shot+2012-05-10+at+4.29.42+PM.png" width="100%" /></a></div>
</div>
<div style="clear: both;">
<br /></div>
<br />
<h3>
Responsive Web Design</h3>
Last summer at the 2011 <a href="http://frontenddesignconference.com/">front-end design conference</a> I had the privilege of having Ethan Marcotte introduce me to the idea of <a href="http://www.abookapart.com/products/responsive-web-design">responsive web design</a>. So, I took that to heart and designed both the public site and the admin area to be responsive. So, if we look at the cat listing page again and compare it between a browser and an iPhone, you can see that the navigation menu has collapsed and the table has dropped several columns.<br />
<br />
<div style="width: 100%;">
<div style="float: left; width: 60%;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEipkO1_FvcZXfnaHI2H_N21AeGSU2_HFcl_ehQjpbfq5sFRbD4rKHlBcum80_NB3Z2j9i8rStmht3EX4VjQ5-gWh8sQmgtOFqAD_OfreqT02bVonaoI3mZcY3ITnck6R3XdAAOE/s1600/Screen+Shot+2012-05-10+at+4.05.04+PM.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEipkO1_FvcZXfnaHI2H_N21AeGSU2_HFcl_ehQjpbfq5sFRbD4rKHlBcum80_NB3Z2j9i8rStmht3EX4VjQ5-gWh8sQmgtOFqAD_OfreqT02bVonaoI3mZcY3ITnck6R3XdAAOE/s320/Screen+Shot+2012-05-10+at+4.05.04+PM.png" width="100%" /></a></div>
<div style="float: right; width: 39%;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj25Ev1U4CGbwscJ7YPE-B7mbxmcCCkR3DE8MTUcUXLP1U5A76ybWgbpbSVwXKPrSeFbQb5ug8mBhvx7tZLsNgbll6FtPGdtb2zdZ4ekEqBlnu_ADpvnKUhMW9C726DhDtuaHPR/s1600/Screen+Shot+2012-05-10+at+4.48.19+PM.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj25Ev1U4CGbwscJ7YPE-B7mbxmcCCkR3DE8MTUcUXLP1U5A76ybWgbpbSVwXKPrSeFbQb5ug8mBhvx7tZLsNgbll6FtPGdtb2zdZ4ekEqBlnu_ADpvnKUhMW9C726DhDtuaHPR/s320/Screen+Shot+2012-05-10+at+4.48.19+PM.png" width="175px" /></a></div>
</div>
<div style="clear: both;">
There are also intermediate changes for tablets, but it's time I wrap this up.<br />
<br />
I'm only touching on a very few of the page layouts and features. Beyond a new look and feel, the re-write of the application also brought performance improvements. Here is a New Relic report on the week I switched it over from the old to the new.<br />
<br />
Note: Thursday was the transition day, so it should be ignored. The jump in CPU percentage is due to also switching from a shared hosting environment on DreamHost to a Linode slice since DreamHost doesn't support Rails 3.1 at this time -- at least not on my server)<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZ8DcPgv0a5flNaadM64sa1lL0O2TOvqnR9Pwo111u_hw4N181f5evR5mV3tc_milUtVXC7KuhiIdDrMmxkyc2iQccYuxASg-jHf4MPa__OmXZQUiVqdJfgXMAiAA7cOKrcrkK/s1600/performance+before+and+after.jpg" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="232" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZ8DcPgv0a5flNaadM64sa1lL0O2TOvqnR9Pwo111u_hw4N181f5evR5mV3tc_milUtVXC7KuhiIdDrMmxkyc2iQccYuxASg-jHf4MPa__OmXZQUiVqdJfgXMAiAA7cOKrcrkK/s640/performance+before+and+after.jpg" width="640" /></a></div>
<br /></div>
<br />
<br />
I encourage you to take a look at the <a href="http://www.stfrancisrescue.org/">St. Francis Society Animal Rescue</a> public site, particularly if you're in the Tampa Bay area and are interested in a new pet. Be sure to try it out in different sizes to see how the responsive layout works, and please let me know what you think of it.<br />
<br />Unknownnoreply@blogger.com1tag:blogger.com,1999:blog-21222625.post-89323213096978554122012-05-10T14:40:00.003-04:002020-12-18T10:21:23.191-05:00Adding and removing multiple associated sub-models dynamically on a single form in RailsThe last couple blog posts have been about PeepNote ... its beginning and its ending. Now I thought I'd share a snippet of code that I developed for it the form of a simple contact manager.<br />
<br />
One of the things Brian pointed out in his concluding remarks about PeepNote that I referred to in my last post was,<br />
<blockquote class="tr_bq">
As I began interacting more with potential customers I realized that my
target audience was not what I thought it was. It wasn’t people like me
who were heavily using Twitter for career networking and wanted to keep
track of how I met people and what I knew about them. Instead, the only
people that would pay for the service were companies. Companies that
wanted to use it to track potential customers; a CRM.</blockquote>
Part of the CRM functionality that we had incorporated into PeepNote was to add contact information about your peeps. The idea of a contact manager is
well understood by most audiences and are thus commonly used as a
demonstration application. <b>So, what's special about this one and why I'm I writing about it?</b><br />
<br />
This particular contact manager demonstrates
(and improves upon) Ryan Bates <a href="http://media.pragprog.com/titles/fr_arr/multiple_models_one_form.pdf">“Handle Multiple Models in One Form”</a> recipe (#13) from the <a href="https://www.amazon.com/Advanced-Rails-Recipes-Mike-Clark/dp/0978739221">Advanced Rails Recipes</a> book. In Ryan’s original recipe, he added discrete field values (individual
tasks on a to-do list) to the form. Any tasks that did not have an
identifier associated with it was a new task, and any missing task
identifiers from those currently in the database were assumed to be
deleted. This is great when you're essentially adding single values to a list, but what about adding multiple pieces of information that need to stay together as a single entity (like the parts of a contact information - street address, city, state, phone, etc)?<br />
<br />
The solution is to use a JavaScript
variable to assign temporary unique identifiers to newly added nested
models (addresses, phone numbers, urls, etc). These newly added models are assigned a negative
value in order for the contact controller to distinguish between new and
existing records. So, new records with a negative id are added, existing records are updated as necessary, and any records with ids that are no longer in the submitted list are removed.<br />
<br />
This is one of the problems I solved for PeepNote and have extracted into a simple stand-alone Rails application. Rather than provide a lengthy description here, I placed the source code on <a href="https://github.com/pothoven/contacts">GitHub</a> and have a running demo on <a href="https://pothoven-contacts.herokuapp.com/">Heroku</a>.<br />
<br />Unknownnoreply@blogger.com1tag:blogger.com,1999:blog-21222625.post-73994603573603668212012-05-10T14:18:00.001-04:002020-12-18T10:23:32.053-05:00PeepNote conclusionI realize it's been a very long time since my last post, but I am still around.<br />
<br />
In regards to that last post, I thought I'd share a blog entry by my RailsRumble teammate discussing the status of PeepNote:<br />
<br />
PeepNote: The Rumble, the Startup, and now…the Conclusion <div>(http://brianburridge.com/2012/03/12/peepnote-the-conclusion/)</div>Unknownnoreply@blogger.com1tag:blogger.com,1999:blog-21222625.post-12421102600566460222009-08-25T08:38:00.010-04:002020-12-18T10:30:42.111-05:00PeepNotes - my Rails Rumble 2009 entry<a href="http://www.peepnote.com/" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img alt="" border="0" id="BLOGGER_PHOTO_ID_5373889546341609394" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjAoDEg50kFXPp0xB2C7Cmx2jRKMJS_1F33D9PwdBMqDjhzyvjgZhtnkYJqLbDz-ypPM6K_gFlSiGT4FGsyI91mIU9gE6H36TAV6IEy1x3LhDOtJOOfMqcL7X35_H6ZNf7Y6ok-/s400/peepnote-logo.png" style="float: left; height: 53px; margin: 0px 20px 10px 0px; width: 143px;" /></a><br />
<div style="padding-top: 8px;">
This past weekend I took part in <a href="http://r09.railsrumble.com/">Rails Rumble 2009</a> and help create a site called PeepNote.</div>
<br />
<br />
If you're not familiar with Rails Rumble, I'll borrow the introduction from their web site:<br />
<br />
<blockquote>
The Rails Rumble is a 48 hour web application development competition. As a contestant, your team gets one weekend to design, develop, and deploy the best web property that you can, using the awesome power of <a href="https://www.ruby-lang.org/en/">Ruby</a> and <a href="http://rubyonrails.com/">Rails</a>.</blockquote>
<br />
<br />
Two of my teammates have written up nice articles on our experience, so I encourage you to read them:<br />
<ul>
<li>Rails Rumble 09 is over (http://www.brianburridge.com/2009/08/24/rails-rumble-09-is-over/) by Brian Burridge</li>
<br />
<li><a href="http://thevisualclick.com/blog/2009/08/48-hours-the-creation-of-peepnote/">48 Hours & The Creation of PeepNote</a> by Josh Hemsley</li>
</ul>
Unknownnoreply@blogger.com0tag:blogger.com,1999:blog-21222625.post-84234606131331803192009-08-13T10:44:00.007-04:002020-12-18T10:32:36.435-05:00Managing Tags in RailsIn this article I'm going to document a small piece of the administration component of the Ruby Rails Review that I wrote. On Ruby Rails Review we kept a collection of interesting articles regarding Ruby on Rails, and these articles were tagged with various tags so that if you went to browse the articles you could filter them by the various tags.
Of course, there's no reason to implement that whole tagging system ourselves since there are plug-ins for that. To make the articles taggable we used the <a href="http://www.intridea.com/2007/12/4/announcing-acts_as_taggable_on">acts_as_taggable_on</a> plug-in. This article just gives a method for easily editing the tags for a given article which works much like the Blogger labeling I used when writing this article.
For Ruby Rails Review, the article editor form is shown below, though I've dimmed out everything but the tagging sections:
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2LYLlGHHXwzqdeKLZQJ6A_goY1UP4jB01YTZHjTWuhIhGgu_g21NLXeFOvblst8jMgvCzc6XiO9wz7Z7khJWRq1Prm9_hGTC_Q1I_Io0ll32k1AoStJP52M-J9Ifzk5UzPZaq/s1600-h/ArticleEditForm.png" onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}"><img alt="" border="0" id="BLOGGER_PHOTO_ID_5369465593481652098" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2LYLlGHHXwzqdeKLZQJ6A_goY1UP4jB01YTZHjTWuhIhGgu_g21NLXeFOvblst8jMgvCzc6XiO9wz7Z7khJWRq1Prm9_hGTC_Q1I_Io0ll32k1AoStJP52M-J9Ifzk5UzPZaq/s400/ArticleEditForm.png" style="cursor: pointer; display: block; height: 204px; margin: 0px auto 10px; text-align: center; width: 400px;" /></a>
As you can see, there is a text field where you can type in tags in a comma separated list, as well a list of all currently defined tags. Selected tags are shown in blue and underlined to give a visual indication that they are selected, but all tags are clickable to either select or deselect them. As tags from the list at the bottom are selected or deselected, they are added or removed from the text field. If you enter tags in the text field they are either selected or added (or deselected if you remove a tag) from the tag list below.
So, how do we do this?
First, the form code looks like this:
<pre class="code"><% form_for @article_detail, :url => { :action => "update", :id => @article_detail.article_id } do |f| %>
...
<%= f.text_field :tag_list, :onchange => "updateTagList(this.value);" %>
...
<% end %>
<div id="tags" style="margin-top:30px;">
<% @tags.each do |tag|%>
<div class="tag<%= ' selectedTag' if @article_detail.tag_list.index(tag.name) %>" onclick="selectTag(this);"><%= tag.name %></div>
<% end %>
</div>
</pre>
You'll also need a little bit of CSS added to make the selected tags stand out (added to your main.css). I happened to make them blue and underlined, but you can make them highlighted or whatever you please:
<pre>.selectedTag {
color: #247CD4;
text-decoration: underline;
}
</pre>
Finally, add the necessary JavaScript in either your application.js or a separate JavaScript file of you choosing:
<pre class="code">function selectTag(tagElem) {
tagElem.toggleClassName('selectedTag');
var selected_tags = buildTagList();
$('article_detail_tag_list').value = selected_tags;
$('article_detail_tag_list').focus();
};
function updateTagList(selected_tags) {
selected_tags = selected_tags.split(',');
var tags = $H();
$$('.tag').each(function(tag) {
tags.set(tag.innerHTML, tag);
tag.removeClassName("selectedTag");
});
selected_tags.each(function(tagName) {
tagName = tagName.strip();
var tag = tags.get(tagName);
if (tag) {
tag.addClassName("selectedTag");
} else {
$('tags').insert('<div class="tag selectedTag" onclick="selectTag(this);">'+tagName+'</div>');
}
});
};
function buildTagList(tag) {
var selected_tags = [];
$$('.selectedTag').each(function(tagElem) {
selected_tags.push(tagElem.innerHTML);
});
if (tag) {
if (selected_tags.indexOf(tag) === -1) {
selected_tags.push(tag);
} else {
selected_tags = selected_tags.without(tag);
}
}
selected_tags = selected_tags.join(',');
return selected_tags;
}
</pre>
That's it! Now you can easily maintain the tags on your taggable items.Unknownnoreply@blogger.com2