OpenMRS in Chinese!

April 30, 2014

openmrs-chineseVery cool… of course, it’s all Chinese too me.  Here’s Google’s translation back to English:

openmrs-chinese-englishThanks to Yang & team, Harsha, and all who contributed!

 

 

Faster resets for the OpenMRS Demo

March 11, 2014

I’ve long been bothered by how long it takes for our demo site (http://demo.openmrs.org) to be reset.  The site goes down for a few minutes each hour as the database is restored and the application restarted.  I was working on the demo site today in the middle of one of these resets and so I decided to look around for a better solution.

Fortunately, Akshay Suryawanshi from Percona (percona.com) recently shared a nifty rename_db script that can rename a MySQL database in a couple of seconds.  So, we put our credentials and host information into a file (don’t forget chmod 600 .my.cnf to protect those credentials):

.my.cnf

[client]
user=openmrs
password=secret
host=127.0.0.1
port=3306

Assuming the credentials and other settings are correct, then this script should show existing databases:

show-databases

#!/bin/bash
mysql --defaults-extra-file=.my.cnf -e "show databases"

Then we take Akshay’s rename_db script, define DEFAULTS=.my.cnf at the beginning, replace the “-h $1” references with --defaults-extra-file=$DEFAULTS, and remove the initial host parameter:

rename_db

Adapted from Akshay Suryawanshi’s blog post, thanks to the generous folks at Percona for sharing.

#!/bin/bash
# Copyright 2013 Percona LLC and/or its affiliates
# http://www.mysqlperformanceblog.com/2013/12/24/renaming-database-schema-mysql/
#
# Changes
# - Uses --defaults-extra-file for credentials, host, and port settings
set -e
DEFAULTS=.my.cnf
if [ -z "$2" ]; then
    echo "rename_db  "
    echo "(assumes $DEFAULTS defines user, password, host, and port under [client] section)"
    exit 1
fi
db_exists=`mysql --defaults-extra-file=$DEFAULTS -e "show databases like '$2'" -sss`
if [ -n "$db_exists" ]; then
    echo "ERROR: New database already exists $2"
    exit 1
fi
TIMESTAMP=`date +%s`
character_set=`mysql --defaults-extra-file=$DEFAULTS -e "show create database $1\G" -sss | grep ^Create | awk -F'CHARACTER SET ' '{print $2}' | awk '{print $1}'`
TABLES=`mysql --defaults-extra-file=$DEFAULTS -e "select TABLE_NAME from information_schema.tables where table_schema='$1' and TABLE_TYPE='BASE TABLE'" -sss`
STATUS=$?
if [ "$STATUS" != 0 ] || [ -z "$TABLES" ]; then
    echo "Error retrieving tables from $1"
    exit 1
fi
echo "create database $2 DEFAULT CHARACTER SET $character_set"
mysql --defaults-extra-file=$DEFAULTS -e "create database $2 DEFAULT CHARACTER SET $character_set"
TRIGGERS=`mysql --defaults-extra-file=$DEFAULTS $1 -e "show triggers\G" | grep Trigger: | awk '{print $2}'`
VIEWS=`mysql --defaults-extra-file=$DEFAULTS -e "select TABLE_NAME from information_schema.tables where table_schema='$1' and TABLE_TYPE='VIEW'" -sss`
if [ -n "$VIEWS" ]; then
    mysqldump --defaults-extra-file=$DEFAULTS $1 $VIEWS > /tmp/${1}_views${TIMESTAMP}.dump
fi
mysqldump --defaults-extra-file=$DEFAULTS $1 -d -t -R -E > /tmp/${1}_triggers${TIMESTAMP}.dump
for TRIGGER in $TRIGGERS; do
    echo "drop trigger $TRIGGER"
    mysql --defaults-extra-file=$DEFAULTS $1 -e "drop trigger $TRIGGER"
done
for TABLE in $TABLES; do
    echo "rename table $1.$TABLE to $2.$TABLE"
    mysql --defaults-extra-file=$DEFAULTS $1 -e "SET FOREIGN_KEY_CHECKS=0; rename table $1.$TABLE to $2.$TABLE"
done
if [ -n "$VIEWS" ]; then
    echo "loading views"
    mysql --defaults-extra-file=$DEFAULTS $2 < /tmp/${1}_views${TIMESTAMP}.dump
fi
echo "loading triggers, routines and events"
mysql --defaults-extra-file=$DEFAULTS $2 < /tmp/${1}_triggers${TIMESTAMP}.dump
TABLES=`mysql --defaults-extra-file=$DEFAULTS -e "select TABLE_NAME from information_schema.tables where table_schema='$1' and TABLE_TYPE='BASE TABLE'" -sss`
if [ -z "$TABLES" ]; then
    echo "Dropping database $1"
    mysql --defaults-extra-file=$DEFAULTS $1 -e "drop database $1"
fi
if [ `mysql --defaults-extra-file=$DEFAULTS -e "select count(*) from mysql.columns_priv where db='$1'" -sss` -gt 0 ]; then
    COLUMNS_PRIV="    UPDATE mysql.columns_priv set db='$2' WHERE db='$1';"
fi
if [ `mysql --defaults-extra-file=$DEFAULTS -e "select count(*) from mysql.procs_priv where db='$1'" -sss` -gt 0 ]; then
    PROCS_PRIV="    UPDATE mysql.procs_priv set db='$2' WHERE db='$1';"
fi
if [ `mysql --defaults-extra-file=$DEFAULTS -e "select count(*) from mysql.tables_priv where db='$1'" -sss` -gt 0 ]; then
    TABLES_PRIV="    UPDATE mysql.tables_priv set db='$2' WHERE db='$1';"
fi
if [ `mysql --defaults-extra-file=$DEFAULTS -e "select count(*) from mysql.db where db='$1'" -sss` -gt 0 ]; then
    DB_PRIV="    UPDATE mysql.db set db='$2' WHERE db='$1';"
fi
if [ -n "$COLUMNS_PRIV" ] || [ -n "$PROCS_PRIV" ] || [ -n "$TABLES_PRIV" ] || [ -n "$DB_PRIV" ]; then
    echo "IF YOU WANT TO RENAME the GRANTS YOU NEED TO RUN ALL OUTPUT BELOW:"
    if [ -n "$COLUMNS_PRIV" ]; then echo "$COLUMNS_PRIV"; fi
    if [ -n "$PROCS_PRIV" ]; then echo "$PROCS_PRIV"; fi
    if [ -n "$TABLES_PRIV" ]; then echo "$TABLES_PRIV"; fi
    if [ -n "$DB_PRIV" ]; then echo "$DB_PRIV"; fi
    echo "    flush privileges;"
fi

We use the following script to backup the OpenMRS database:

backup-openmrs

#!/bin/bash
mysqldump --defaults-extra-file=.my.cnf --add-drop-database --extended-insert \
    --single-transaction openmrs > openmrs.sql

Then we create the following scripts:

drop-openmrs

#!/bin/bash
mysql --defaults-extra-file=.my.cnf -e "drop database openmrs"

restore-openmrs1

#!/bin/bash
mysql --defaults-extra-file=.my.cnf \
    -e "create database openmrs1 DEFAULT CHARACTER SET utf8"
mysql --defaults-extra-file=.my.cnf openmrs1 < openmrs.sql

replace-openmrs

#!/bin/bash
./drop-openmrs
./rename_db openmrs1 openmrs

Now, when we are preparing to reset the openmrs database, we can execute the restore-openmrs1 script, which will place a fresh copy of the default OpenMRS data into the openmrs1 database (restoring the database can take several seconds, but since we are doing this into openmrs1 and the demo is using openmrs, we can perform this step before involving the demo site). Then, when we are ready to reset the database, simply executing replace-openmrs will reset the openmrs database in 2-3 seconds or less. Given that the demo data is a non-production system, we could even perform this data replacement without restarting the demo application.

Now, instead of taking 30-90 seconds or more, the OpenMRS Demo can be reset in 2-3 seconds or less.

Thank you Akshay Suryawanshi and the folks at Percona for sharing!

OpenMRS SDK

October 24, 2013

I’m very excited about the OpenMRS SDK and what it can mean for the OpenMRS Developer Community.

Chris Niesel (h3llborn) brought this beautiful creature to life during Google Summer of Code 2013 under the mentorship of Rafał Korytkowski.  Strong work, guys!  Looking forward to watching the SDK grow into both the first thing a new OpenMRS Developer touches and an invaluable tool for daily development of our most experienced developers!

Alfred Workflows

April 3, 2013

If you own a Mac and haven’t discovered Alfred 2 yet, well I’m sorry.  Combined with its PowerPack, it’s a formidable tool for doing things quickly on the Mac.  I’ve used QuickSilver and LaunchBar.  I held out for a long time as a staunch LaunchBar user, but recently made the switch to Alfred 2 and I’ve got it doing anything everything I need (and boy do I need a lot… I’m lazy and I’ll love keyboard shortcuts).  The only shortcoming with Alfred is its clipboard history, which is limited to text (no images… bummer).  LaunchBar does the clipboard history just right, so I map Alt+Cmd+K to LaunchBar’s clipboard history and I’m good to go.  Sure, LaunchBar is an expensive tool just for clipboard history, but I already owned it and why use Alfred’s crappy clipboard history when I have access to LaunchBar? 🙂

Anyway, I thought I’d share a few of my (now ~24) Alfred 2 workflows for anyone interested:

icon Arrows
Easily type arrow characters (↑, ↓, ←, →) using HTML entity names: uarr, darr, larr, and rarr.
IRC Cloud IRC Cloud
Open IRC Cloud in Chrome with irc.  If you’ve already got IRC Cloud in a tab, it doesn’t open it in a second tab.
icon OpenMRS
A workflow for OpenMRS developers with a few handy shortcuts.

  • op — launches the OpenMRS wiki
  • ot [ticket] — launches OpenMRS JIRA. Naming a ticket goes straight to that ticket; giving just a number will open the corresponding TRUNK ticket; leaving off the ticket just takes you to JIRA.
  • dev — opens the OpenMRS Developers Forum page
  • raf — types Rafał’s name properly into your editor (and into your clipboard in case you need it again)
  • paul – pastes “Paul is a dork” into your editor
icon Volume Controls
Provides some shortcuts for controlling your volume. You thought you were lazy? I know there are dedicated keys for this, but they’re way over there. Alfred shortcuts can be habituated (once learned, you just think and it happens) and don’t require your hands to leave the “home row” of the keyboard.

  • vl — volume low (about one “dot” of volume on the Mac)
  • vh — volume high (crank it up!)
  • mute ‐ guess what this does. 🙂

Auditing OpenMRS repos in GitHub

April 1, 2013

OpenMRS has some basic conventions for its repositories in GitHub (within the openmrs org).  Basically, we add four teams to every repo (owners, full committers, partial committers, and repo owners) and we disable wiki & issues on the repo (since we already have a place for wiki & issues).

It’s easy for these things to get overlooked as new repos are added to the org in GitHub, so I made a little Groovy script to manually audit the repos.  Nothing fancy and it doesn’t really warrant a repo of its own, but I want to get the code off my laptop… so I’m blogging it. 🙂

The output should look something like this:

$ groovy AuditOpenMRSRepos.groovy
OpenMRS org has 78 repos
Owners team has 78 repos, missing none
Full Committers team has 78 repos, missing none
Partial Committers team has 77 repos, missing none
Repo Owners team has 77 repos, missing none
wikis or issues (should be empty): []

Audit OpenMRS repos

#!/usr/bin/env groovy

import groovyx.net.http.RESTClient
import org.apache.http.*
import org.apache.http.protocol.*
import groovyx.net.http.*

@Grab(group='org.codehaus.groovy.modules.http-builder', module='http-builder', version='0.7.1')

def scriptDir = new File(getClass().protectionDomain.codeSource.location.path).parent
def token = new File("$scriptDir/github.token").text.trim()

// initialze a new builder and give a default URL
def github = new RESTClient( 'https://api.github.com' ).with {
  client.addRequestInterceptor(
    [process: { HttpRequest request, HttpContext context ->
        // using httpbuilders auth mechanism doesn't work, do it manually
        request.setHeader("Authorization", "token $token")
    }] as HttpRequestInterceptor
  )
  client.params.setIntParameter('http.connection.timeout', 5000)
  client.params.setIntParameter('http.socket.timeout', 5000)
  delegate
}

def getNext(response) {
  def next = null
  for (link in response.getHeaders('Link')?.value[0]?.split(',')) {
    def matcher = link =~ /< (.*)\?page=(\d+)>; rel="next"/
    if (matcher.matches()) {
      next = [url:matcher[0][1], page:matcher[0][2]]
    }
  }
  next
}

def fromGithub = { path ->
  def resp = github.get(path: path, headers:['User-Agent':'Groovy'])
  def data = resp.data
  def next = getNext(resp)
  while (next) {
    resp = github.get(path: next.url, query:[page: next.page], headers:['User-Agent':'Groovy'])
    data += resp.data
    next = getNext(resp)
  }
  data
}

def addRepoToTeam = { repo, team ->
  def resp = github.put(path: "/teams/$team.id/repos/openmrs/$repo.name",
  	headers:['User-Agent':'Groovy'])
}

def editRepo = { repo ->
  def body = """{"name":"$repo.name", "has_issues":false, "has_wiki":false}"""
  def resp = github.patch(path: "/repos/openmrs/$repo.name", body: body, 
    requestContentType:ContentType.URLENC, headers:['User-Agent':'Groovy'])
}

repos = fromGithub('/orgs/openmrs/repos')
teams = fromGithub('/orgs/openmrs/teams').findAll{ it.name != 'Transfer Team' && it.name != 'Release-test' }
println "OpenMRS org has ${repos.size()} repos"
for (team in teams) {
  teamRepoNames = fromGithub("/teams/$team.id/repos").collect{ it.name }
  missing = repos.findAll{ !(it.name in teamRepoNames) && it.name != 'openmrs-core' }
  print "$team.name team has ${teamRepoNames.size()} repos, "
  println missing.size() > 0 ? "missing from these: ${missing.collect{it.name}}" : "missing none"
  for (repo in missing) {
    print "fixing..."
    addRepoToTeam(repo, team)
    println "done."
  }
}

wikisOrIssues = repos.findAll{ it.has_wiki || it.has_issues }
println "wikis or issues (should be empty): ${wikisOrIssues.collect{it.name}}"
for (repo in wikisOrIssues) {
  print "fixing..."
  editRepo(repo)
  println "done."
}

OpenMRS Licensing

March 23, 2013

OpenMRS

Background

In the process of upgrading the software license for OpenMRS, it seems like a good time to review how we got here and why we’re changing our license.  Here’s a brief history of OpenMRS Licensing…

OpenMRS Public License 1.0

Ok.  Let’s stop here for a second.  At this point, in 2007, OpenMRS transforms from a shared, unlicensed, pile of code within a public Subversion repository into officially licensed open-source software.  So, what is this OpenMRS Public License and where did it come from?  With the help of local lawyers, we hired a lawyer with expertise in open source licensing and described our goals:

  • Keep the platform openly available… forever, but avoid being so copyleft as to scare away commercial interests – i.e., a key goal of the platform is to enable local capacity.
  • Provide indemnity for medico-legal issues.
  • Allow modules to be licensed separately.
  • Require anyone changing the core platform to share those changes, while leaving open the possibility for an exception iff an entity seeking an exception offers something in exchange that the community deems worthy (e.g., BigCo wants to commercialize an adapted version of OpenMRS as a closed product, but is willing to offer resources for a dozen dedicated FTEs to the open-source effort in perpetuity, and jumps through enough hoops to satisfy the OpenMRS community)

The lawyer took these goals, reviewed the available open source licenses at the time, and suggested the Mozilla Public License 1.1 with a few tweaks to fit our specific needs.  So, we created the OpenMRS Public License 1.0 as a slightly modified version of MPL 1.1 and applied it to all of our code.

Living with OPL 1.0

OpenMRS Public License 2.0 Mozilla Public License 2.0 + Disclaimer

In 2013, with some hard work by Paul and help from OSI, Luis Villa, and some other lawyers, we discover that the Mozilla Public License 2.0 with a disclaimer could meet all of our needs, so we proposed the idea to the community.  Overall, people in the community are pleased to see us adopting an OSI-approved license.

OSI LogoOkay, OpenMRS on an OSI-approved license… at last.  That’s cool.  But why Mozilla Public License 2.0 (MPL 2.0)?

 

See our license at http://openmrs.org/license.

 

Preach Open Source; when necessary, mention code.

March 19, 2013

I was recently reminded of the quote that so often is (mis)attributed to St. Francis of Assisi:

Preach often; when necessary, use words.

Without getting religious, I believe and hope OpenMRS follows this dictum: spend more time & effort improving health in resource poor environments than we spend talking about it.  And, for OpenMRS, we choose to do our work openly.  Why?  Because “Open Source” is not so much about sharing code as it is about sharing experiences, teaching & learning from each other, and embracing collaboration & open methodologies… in short, open behavior.  It took a while to learn that distinction.  Now when I hear people fussing over code, it feels petty.  One of the most important lessons that I’ve learned from the OpenMRS Community:

The value is not in the code; it’s in the people.

We could switch to a completely different programming language or a new shiny platform could (and likely will) come along some day that’s far better than what we’ve done.  All of our precious code will fade away.  That’s fine.  As long as we have our people.  Are you running a development shop?  Have you calculated how much you’ve invested in your “product” and found yourself thinking about the code?  Wrong.  Your investment is in people.  Give me the choice between good code and a good coder and I will take the coder 100 times out of 100.  In the end, code is just a tool… property.  And property can be replaced.  Love your coders.  Treat them well and invest in them.  Life will be good.  And while you’re doing that, consider working openly.

The growing OpenMRS Development Glossary

January 4, 2013

As we work to organize & improve the development work we are doing for OpenMRS, we’re starting to use (and sometimes misuse) a growing list of terminology. I thought I’d dump them out here (to get them out of my head, to be able to come back & look at them, and maybe to get comments from folks on what we’ve got wrong).  Many of these are drawn from our (growing) experience with agile methodology.  Since I’m not taking the time to look up “official” definitions, I’m sure these are imprecise and I wouldn’t be surprised to learn that we’re doing what others were doing 10 years ago. 🙂

What gets done:

How it gets done:

Who does the work:

Thoughts or comments are welcome.

Partial checkout with Subversion

June 6, 2012

I wanted to check out OpenMRS trunk and a couple branches from the subversion repository within the same local copy so that I could apply a set of changes to trunk and backport it to a couple prior versions in a single commit. Typically, when you check out a directory, you get everything underneath. But OpenMRS trunk and the branches I wanted are only a few of the bazillion folders and files underneath http://svn.openmrs.org/openmrs/. Checking out all of those folders would take a long time, place an unnecessary tax on the OpenMRS repository, and end up transferring a bazillion files that I didn’t need. While it’s not obvious, there is a way to do this with Subversion.  The answer is in Subversions sparse directories feature. Thank you, stackoverflow! Here’s how I checked out only a subset of folders (trunk and a couple branches) from subversion:

$ svn co --depth empty http://svn.openmrs.org/openmrs
$ svn update --set-depth infinity openmrs/trunk
$ svn update --set-depth empty openmrs/branches
$ svn update --set-depth infinity openmrs/branches/1.8.x
$ svn update --set-depth infinity openmrs/branches/1.9.x

jsonpretty: handy when playing with web services

October 17, 2011

I just ran into a handy little utility: jsonpretty.

$sudo gem install json jsonpretty

While

$curl -i http://localhost:8081/openmrs-standalone/ws/rest/v1/catalog

gives you something like this:

{"catalog":[{"name":"Cohort","operations":[{"name":"GET http://localhost:8081/openmrs/ws/rest/v1/cohort?q","description":"Fetch all non-retired that match this parameter"},{"name":"GET http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}","description":"Fetch by unique uuid"},{"name":"GET http://localhost:8081/openmrs/ws/rest/v1/cohort","description":"Fetch all non-retired"},{"name":"POST http://localhost:8081/openmrs/ws/rest/v1/cohort","description":"Create with properties in request"},{"name":"POST http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}","description":"Edit with given uuid, only modifying properties in request"},{"name":"DELETE http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}?!purge","description":"Delete this object from the database"},{"name":"DELETE http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}?purge","description":"Delete this object from the database"}],"url":"http://localhost:8081/openmrs/ws/rest/v1/cohort","representations":[{"name":"ref","properties":["uuid","display","links"]},{"name":"default","properties":["uuid","name","description","voided","memberIds","links"]},{"name":"full","properties":["uuid","name","description","memberIds","voided","auditInfo","links"]}]},{"name":"CohortMember","operations":[{"name":"GET http://localhost:8081/openmrs/ws/rest/v1/cohort/{parentUuid}/members/{uuid}","description":"Fetch by unique uuid"},{"name":"GET http://localhost:8081/openmrs/ws/rest/v1/cohort/{parentUuid}/members","description":"Fetch all non-retired"},{"name":"POST http://localhost:8081/openmrs/ws/rest/v1/cohort/{parentUuid}/members","description":"Create with properties in request"},{"name":"POST http://localhost:8081/openmrs/ws/rest/v1/cohort/{parentUuid}/members/{uuid}","description":"Edit with given uuid, only modifying properties in request"},{"name":"DELETE http://localhost:8081/openmrs/ws/rest/v1/cohort/{parentUuid}/members/{uuid}?!purge","description":"Delete this object from the database"},{"name":"DELETE http://localhost:8081/openmrs/ws/rest/v1/cohort/{parentUuid}/members/{uuid}?purge","description":"Delete this object from the database"}],...

adding jsonpretty:

$curl -i http://localhost:8081/openmrs-standalone/ws/rest/v1/catalog | jsonpretty

gets you something much prettier:

{
  "catalog": [
    {
      "name": "Cohort",
      "operations": [
        {
          "name": "GET http://localhost:8081/openmrs/ws/rest/v1/cohort?q",
          "description": "Fetch all non-retired that match this parameter"
        },
        {
          "name": "GET http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}",
          "description": "Fetch by unique uuid"
        },
        {
          "name": "GET http://localhost:8081/openmrs/ws/rest/v1/cohort",
          "description": "Fetch all non-retired"
        },
        {
          "name": "POST http://localhost:8081/openmrs/ws/rest/v1/cohort",
          "description": "Create with properties in request"
        },
        {
          "name": "POST http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}",
          "description": "Edit with given uuid, only modifying properties in request"
        },
        {
          "name": "DELETE http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}?!purge",
          "description": "Delete this object from the database"
        },
        {
          "name": "DELETE http://localhost:8081/openmrs/ws/rest/v1/cohort/{uuid}?purge",
          "description": "Delete this object from the database"
        }
      ],
      "url": "http://localhost:8081/openmrs/ws/rest/v1/cohort",
      "representations": [
        {
          "name": "ref",
          "properties": [
            "uuid",
            "display",
            "links"
          ]
        },
        {
          "name": "default",
          "properties": [
            "uuid",
            "name",
            "description",
            "voided",
            "memberIds",
            "links"
          ]
        },
        {
          "name": "full",
          "properties": [
            "uuid",
            "name",
            "description",
            "memberIds",
            "voided",
            "auditInfo",
            "links"
          ]
        }
      ]
    },
    ...

Nice! Thank you jsonpretty! 🙂