[mb-commits] branch, data-nes, updated. Re-add Controller::Role::Wikipedia to works

MusicBrainz Git Server <[email protected]> Fri, 01 Feb 2013 13:31:50 +0000
Newsgroups gmane.comp.audio.musicbrainz.cvs
Message-ID <E1U1Gic-0005v4-FM@wiley>
The branch, data-nes has been updated
  discards  http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=2bf3edede949aed341e01d51e9bee6361c993da4 (commit)
  discards  http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=d2762d22a43038128404459b15b8f108a0133da0 (commit)
  discards  http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=3626afd6a63585587e98af2190aec9bd73fb4f12 (commit)
       via  http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=77e3ac59a922f99be71899f5cd62c55b31801578 (commit)
       via  http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=8abed7c93e212416f8895299f002d655255cbfb6 (commit)
       via  http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=f8a1554b758371d940896d5828c3a28add79745b (commit)

This update added new revisions after undoing existing revisions.  That is
to say, the old revision is not a strict subset of the new revision.  This
situation occurs when you --force push a change and generate a repository
containing something like this:

 * -- * -- B -- O -- O -- O (2bf3edede949aed341e01d51e9bee6361c993da4)
            \
             N -- N -- N (77e3ac59a922f99be71899f5cd62c55b31801578)

When this happens we assume that you've already had alert emails for all
of the O revisions, and so we here report only the revisions in the N
branch from the common base, B.

Summary of changes:
 lib/MusicBrainz/Server/Controller/Work.pm |    8 ++------
 lib/MusicBrainz/Server/Data/NES/Work.pm   |    1 +
 2 files changed, 3 insertions(+), 6 deletions(-)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 77e3ac59a922f99be71899f5cd62c55b31801578
Author: Oliver Charles <[email protected]>
Date:   Fri Feb 1 14:44:46 2013 +0000

    Re-add Controller::Role::Wikipedia to works

diff --git a/lib/MusicBrainz/Server/Controller/Work.pm b/lib/MusicBrainz/Server/Controller/Work.pm
index d6789c1..8281964 100644
--- a/lib/MusicBrainz/Server/Controller/Work.pm
+++ b/lib/MusicBrainz/Server/Controller/Work.pm
@@ -29,7 +29,7 @@ with 'MusicBrainz::Server::Controller::Role::EditListing';
 with 'MusicBrainz::Server::Controller::Role::Rating';
 with 'MusicBrainz::Server::Controller::Role::Relationship';
 with 'MusicBrainz::Server::Controller::Role::Tag';
-# with 'MusicBrainz::Server::Controller::Role::WikipediaExtract';
+with 'MusicBrainz::Server::Controller::Role::WikipediaExtract';
 
 sub base : Chained('/') PathPart('work') CaptureArgs(0) { }
 
diff --git a/lib/MusicBrainz/Server/Data/NES/URL.pm b/lib/MusicBrainz/Server/Data/NES/URL.pm
index e8a8fad..edebca6 100644
--- a/lib/MusicBrainz/Server/Data/NES/URL.pm
+++ b/lib/MusicBrainz/Server/Data/NES/URL.pm
@@ -2,6 +2,7 @@ package MusicBrainz::Server::Data::NES::URL;
 use Moose;
 use namespace::autoclean;
 
+use Class::Load;
 use MusicBrainz::Server::Entity::Tree::URL;
 use MusicBrainz::Server::Entity::URL;
 
@@ -10,12 +11,73 @@ with 'MusicBrainz::Server::Data::NES::CoreEntity' => {
     root => '/url'
 };
 
+my %URL_SPECIALIZATIONS = (
+    '45cat'           => qr{^https?://(?:www.)?45cat.com/}i,
+    'Allmusic'        => qr{^https?://(?:www.)?allmusic.com/}i,
+    'ASIN'            => qr{^https?://(?:www.)?amazon(.*?)(?:\:[0-9]+)?/.*/([0-9B][0-9A-Z]{9})(?:[^0-9A-Z]|$)}i,
+    'BBCMusic'        => qr{^https?://(?:www.)?bbc.co.uk/music/}i,
+    'Canzone'         => qr{^https?://(?:www.)?discografia.dds.it/}i,
+    'CDBaby'          => qr{^https?://(?:www.)?cdbaby.com/}i,
+    'DHHU'            => qr{^https?://(?:www.)?dhhu.dk/}i,
+    'Discogs'         => qr{^https?://(?:www.)?discogs.com/}i,
+    'DiscosDoBrasil'  => qr{^https?://(?:www.)?discosdobrasil.com.br/}i,
+    'Encyclopedisque' => qr{^https?://(?:www.)?encyclopedisque.fr/}i,
+    'ESTERTallinn'    => qr{^https?://tallinn.ester.ee/}i,
+    'ESTERTartu'      => qr{^https?://tartu.ester.ee/}i,
+    'Facebook'        => qr{^https?://(?:www.)?facebook.com/}i,
+    'IBDb'            => qr{^https?://(?:www.)?ibdb.com/}i,
+    'IMDb'            => qr{^https?://(?:www.)?imdb.com/}i,
+    'IMSLP'           => qr{^https?://(?:www.)?imslp.org/wiki/}i,
+    'IOBDb'           => qr{^https?://(?:www.)?lortel.org/}i,
+    'InternetArchive' => qr{^https?://(?:www.)?archive.org/details/}i,
+    'ISRCTW'          => qr{^https?://(?:www.)?isrc.ncl.edu.tw/}i,
+    'Jamendo'         => qr{^https?://(?:www.)?jamendo.com/}i,
+    'LastFM'          => qr{^https?://(?:www.)?last.fm/}i,
+    'Lieder'          => qr{^https?://(?:www.)?recmusic.org/lieder/}i,
+    'LyricWiki'       => qr{^https?://lyrics.wikia.com/}i,
+    'MetalArchives'   => qr{^https?://(?:www.)?metal-archives.com/}i,
+    'MusicMoz'        => qr{^https?://(?:www.)?musicmoz.org/}i,
+    'MusikSammler'    => qr{^https?://(?:www.)?musik-sammler.de/}i,
+    'MySpace'         => qr{^https?://(?:www.)?myspace.com/}i,
+    'OCReMix'         => qr{^https?://(?:www.)?ocremix.org/}i,
+    'Ozon'            => qr{^https?://(?:www.)?ozon.ru/}i,
+    'PsyDB'           => qr{^https?://(?:www.)?psydb.net/}i,
+    'PureVolume'      => qr{^https?://(?:www.)?purevolume.com/}i,
+    'Rateyourmusic'   => qr{^https?://(?:www.)?rateyourmusic.com/}i,
+    'RockInChina'     => qr{^https?://(?:www.)?rockinchina.com/}i,
+    'Rolldabeats'     => qr{^https?://(?:www.)?rolldabeats.com/}i,
+    'SecondHandSongs' => qr{^https?://(?:www.)?secondhandsongs.com/}i,
+    'Songfacts'       => qr{^https?://(?:www.)?songfacts.com/}i,
+    'SoundCloud'      => qr{^https?://(?:www.)?soundcloud.com/}i,
+    'SpiritOfMetal'   => qr{^https?://(?:www.)?spirit-of-metal.com/}i,
+    'Theatricalia'    => qr{^https?://(?:www.)?theatricalia.com/}i,
+    'Trove'           => qr{^https?://(?:www.)?trove.nla.gov.au/}i,
+    'Twitter'         => qr{^https?://(?:www.)?twitter.com/}i,
+    'VGMdb'           => qr{^https?://(?:www.)?vgmdb.net/}i,
+    'Wikipedia'       => qr{^https?://([\w-]{2,})\.wikipedia.org/wiki/}i,
+    'Worldcat'        => qr{^https?://(?:www.)?worldcat.org/}i,
+    'YouTube'         => qr{^https?://(?:www.)?youtube.com/}i,
+);
+
+sub determine_url_class {
+    my $url = shift;
+    for my $class (keys %URL_SPECIALIZATIONS) {
+        my $regex = $URL_SPECIALIZATIONS{$class};
+        next unless $url =~ $regex;
+
+        $class = "MusicBrainz::Server::Entity::URL::$class";
+        Class::Load::load_class($class);
+        return $class if ($url =~ $regex);
+    }
+    return 'MusicBrainz::Server::Entity::URL';
+};
+
 sub find_or_insert {
     my ($self, $edit, $editor, $url) = @_;
     $self->create(
         $edit, $editor,
         MusicBrainz::Server::Entity::Tree::URL->new(
-            url => MusicBrainz::Server::Entity::URL->new(
+            url => determine_url_class($url)->new(
                 url => $url
             )
         )
@@ -33,7 +95,7 @@ sub tree_to_json {
 sub map_core_entity {
     my ($self, $response) = @_;
     my %data = %{ $response->{data} };
-    return MusicBrainz::Server::Entity::URL->new(
+    return determine_url_class($data{url})->new(
         url => $data{url},
 
         gid => $response->{mbid},

commit 8abed7c93e212416f8895299f002d655255cbfb6
Author: Oliver Charles <[email protected]>
Date:   Fri Feb 1 13:59:37 2013 +0000

    Add some very basic support for viewing URL relationships on work index pages

diff --git a/lib/MusicBrainz/Server/Controller/Edit/Relationship.pm b/lib/MusicBrainz/Server/Controller/Edit/Relationship.pm
index dec899b..2a3ba7c 100644
--- a/lib/MusicBrainz/Server/Controller/Edit/Relationship.pm
+++ b/lib/MusicBrainz/Server/Controller/Edit/Relationship.pm
@@ -5,6 +5,7 @@ BEGIN { extends 'MusicBrainz::Server::Controller' };
 
 use MusicBrainz::Server::Constants qw( $EDIT_RELATIONSHIP_DELETE );
 use MusicBrainz::Server::Data::Utils qw( type_to_model );
+use MusicBrainz::Server::Entity::Link;
 use MusicBrainz::Server::Entity::NES::Relationship;
 use MusicBrainz::Server::Entity::Tree::Work;
 use MusicBrainz::Server::Edit::Relationship::Delete;
@@ -346,8 +347,11 @@ sub create_url : Local RequireAuth Edit
                 MusicBrainz::Server::Entity::Tree::Work->new(
                     relationships => [
                         MusicBrainz::Server::Entity::NES::Relationship->new(
-                            link_type_id => $form->field('link_type_id')->value,
-                            target => $url
+                            link => MusicBrainz::Server::Entity::Link->new(
+                                type_id => $form->field('link_type_id')->value
+                            ),
+                            target => $url,
+                            target_type => 'url'
                         )
                     ]
                 )
diff --git a/lib/MusicBrainz/Server/Controller/Role/Relationship.pm b/lib/MusicBrainz/Server/Controller/Role/Relationship.pm
index 16cb226..8a4b59f 100644
--- a/lib/MusicBrainz/Server/Controller/Role/Relationship.pm
+++ b/lib/MusicBrainz/Server/Controller/Role/Relationship.pm
@@ -66,7 +66,9 @@ after 'load' => sub {
     my ($self, $c) = @_;
     my $entity = $c->stash->{entity};
     if ($c->action->name ne 'relationships') {
-        $c->model('Relationship')->load_subset([ 'url' ], $entity);
+        $c->model('MB')->with_nes_transaction(sub {
+            $c->model($self->{model})->load_relationships($entity)
+        });
     }
 };
 
diff --git a/lib/MusicBrainz/Server/Controller/Work.pm b/lib/MusicBrainz/Server/Controller/Work.pm
index 8d38260..d6789c1 100644
--- a/lib/MusicBrainz/Server/Controller/Work.pm
+++ b/lib/MusicBrainz/Server/Controller/Work.pm
@@ -27,12 +27,10 @@ with 'MusicBrainz::Server::Controller::Role::Cleanup';
 with 'MusicBrainz::Server::Controller::Role::Details';
 with 'MusicBrainz::Server::Controller::Role::EditListing';
 with 'MusicBrainz::Server::Controller::Role::Rating';
+with 'MusicBrainz::Server::Controller::Role::Relationship';
 with 'MusicBrainz::Server::Controller::Role::Tag';
-# with 'MusicBrainz::Server::Controller::Role::Relationship';
 # with 'MusicBrainz::Server::Controller::Role::WikipediaExtract';
 
-use aliased 'MusicBrainz::Server::Entity::ArtistCredit';
-
 sub base : Chained('/') PathPart('work') CaptureArgs(0) { }
 
 after 'load' => sub
@@ -58,14 +56,12 @@ sub show : PathPart('') Chained('load')
     $c->model('Language')->load($work);
 
     # Need to call relationships for overview page
-    # $self->relationships($c); NES
+    $self->relationships($c);
 
     $c->stash->{template} = 'work/index.tt';
 }
 
-# NES - originally:
-# for my $action (qw( relationships aliases tags details )) {
-for my $action (qw( aliases tags details )) {
+for my $action (qw( relationships aliases tags details )) {
     after $action => sub {
         my ($self, $c) = @_;
         my $work = $c->stash->{work};
diff --git a/lib/MusicBrainz/Server/Data/NES/CoreEntity.pm b/lib/MusicBrainz/Server/Data/NES/CoreEntity.pm
index 9df8568..590ece5 100644
--- a/lib/MusicBrainz/Server/Data/NES/CoreEntity.pm
+++ b/lib/MusicBrainz/Server/Data/NES/CoreEntity.pm
@@ -51,7 +51,7 @@ role {
     method get_by_gid => sub {
         my ($self, $gid) = @_;
         return $self->_new_from_core_entity(
-            $self->request('/work/find-latest', { mbid => $gid }))
+            $self->request($params->root . '/find-latest', { mbid => $gid }))
     };
 
     method _new_from_core_entity => sub {
diff --git a/lib/MusicBrainz/Server/Data/NES/Work.pm b/lib/MusicBrainz/Server/Data/NES/Work.pm
index 9e5dbde..4cd0d5a 100644
--- a/lib/MusicBrainz/Server/Data/NES/Work.pm
+++ b/lib/MusicBrainz/Server/Data/NES/Work.pm
@@ -1,7 +1,10 @@
 package MusicBrainz::Server::Data::NES::Work;
+use feature 'switch';
 use Moose;
 
+use List::UtilsBy qw( partition_by );
 use MusicBrainz::Server::Data::Utils qw( partial_date_to_hash );
+use MusicBrainz::Server::Entity::NES::Relationship;
 use MusicBrainz::Server::Entity::Work;
 use MusicBrainz::Server::WebService::Serializer::JSON::2::Utils qw( boolean );
 
@@ -61,14 +64,12 @@ sub tree_to_json {
             }, @{ $tree->aliases }
         ],
         relationships => {
-            url => [
+            partition_by { $_->{target_type} }
                 map +{
                     target => $_->target->gid,
-                    type => $_->link_type_id
-                }, grep {
-                    $_->target->isa('MusicBrainz::Server::Entity::URL')
-                } @{ $tree->relationships }
-            ]
+                    type => $_->link_type_id,
+                    target_type => $_->target_type
+                }, @{ $tree->relationships }
         }
     );
 }
@@ -127,6 +128,39 @@ sub get_annotation {
     )->{annotation};
 }
 
+sub get_relationships {
+    my ($self, $revision) = @_;
+    my @rels =
+        map {
+            my $rel = $_;
+            my $target;
+            given ($rel->{'target-type'}) {
+                when (/url/) {
+                    $target = $self->c->model('NES::URL')->get_by_gid($rel->{target});
+                }
+            }
+
+            MusicBrainz::Server::Entity::NES::Relationship->new(
+                target => $target,
+                target_gid => $rel->{target},
+                link => MusicBrainz::Server::Entity::Link->new(
+                    type_id => $rel->{type},
+                    direction => $MusicBrainz::Server::Entity::NES::Relationship::DIRECTION_BACKWARD
+                ),
+                target_type => $rel->{'target-type'},
+            );
+        } @{
+            $self->request(
+                '/work/view-relationships',
+                { revision => $revision->revision_id }
+            )
+        };
+
+    $self->c->model('LinkType')->load(map { $_->link } @rels);
+
+    return \@rels;
+}
+
 sub load_annotation {
     my ($self, $work) = @_;
     $work->latest_annotation(
@@ -142,5 +176,12 @@ sub is_empty {
     )->{eligible};
 }
 
+sub load_relationships {
+    my ($self, @works) = @_;
+    for my $work (@works) {
+        $work->relationships($self->get_relationships($work));
+    }
+}
+
 __PACKAGE__->meta->make_immutable;
 1;
diff --git a/lib/MusicBrainz/Server/Entity/NES/Relationship.pm b/lib/MusicBrainz/Server/Entity/NES/Relationship.pm
index a16a8a2..ba22fb4 100644
--- a/lib/MusicBrainz/Server/Entity/NES/Relationship.pm
+++ b/lib/MusicBrainz/Server/Entity/NES/Relationship.pm
@@ -1,14 +1,110 @@
 package MusicBrainz::Server::Entity::NES::Relationship;
 use Moose;
 
+use MusicBrainz::Server::Entity::Types;
+use MusicBrainz::Server::Validation qw( trim_in_place );
+use MusicBrainz::Server::Translation qw( l );
+use Readonly;
+
 has target => (
     is => 'ro',
-    required => 1
 );
 
-has link_type_id => (
+has target_gid => (
+    is => 'ro',
+);
+
+has target_type => (
     is => 'ro',
     required => 1
 );
 
+has link => (
+    is => 'ro',
+);
+
+has 'phrase' => (
+    is => 'ro',
+    builder => '_build_phrase',
+    lazy => 1
+);
+
+has 'verbose_phrase' => (
+    is => 'ro',
+    builder => '_build_verbose_phrase',
+    lazy => 1
+);
+
+Readonly our $DIRECTION_FORWARD  => 1;
+Readonly our $DIRECTION_BACKWARD => 2;
+
+sub _join_attrs
+{
+    my @attrs = map { $_ } @{$_[0]};
+    if (scalar(@attrs) > 1) {
+        my $a = pop(@attrs);
+        my $b = join(l(", "), @attrs);
+        return l("{b} and {a}", {b => $b, a => $a});
+    }
+    elsif (scalar(@attrs) == 1) {
+        return $attrs[0];
+    }
+    return '';
+}
+
+has direction => (
+    default => $DIRECTION_FORWARD,
+    is => 'ro',
+);
+
+sub _build_phrase {
+    my ($self) = @_;
+    $self->_interpolate(
+        $self->direction == $DIRECTION_FORWARD
+            ? $self->link->type->l_link_phrase()
+            : $self->link->type->l_reverse_link_phrase());
+}
+
+sub _build_verbose_phrase {
+    my ($self) = @_;
+    $self->_interpolate($self->link->type->short_link_phrase);
+}
+
+sub _interpolate
+{
+    my ($self, $phrase) = @_;
+
+    my @attrs = $self->link->all_attributes;
+    my %attrs;
+    foreach my $attr (@attrs) {
+        my $name = lc $attr->root->name;
+        my $value = $attr->l_name();
+        if (exists $attrs{$name}) {
+            push @{$attrs{$name}}, $value;
+        }
+        else {
+            $attrs{$name} = [ $value ];
+        }
+    }
+
+    my $replace_attrs = sub {
+        my ($name, $alt) = @_;
+        if (!$alt) {
+            return '' unless exists $attrs{$name};
+            return _join_attrs($attrs{$name});
+        }
+        else {
+            my ($alt1, $alt2) = split /\|/, $alt;
+            return $alt2 || '' unless exists $attrs{$name};
+            my $attr = _join_attrs($attrs{$name});
+            $alt1 =~ s/%/$attr/eg;
+            return $alt1;
+        }
+    };
+    $phrase =~ s/{(.*?)(?::(.*?))?}/$replace_attrs->(lc $1, $2)/eg;
+    trim_in_place($phrase);
+
+    return $phrase;
+}
+
 1;
diff --git a/lib/MusicBrainz/Server/Entity/Role/Linkable.pm b/lib/MusicBrainz/Server/Entity/Role/Linkable.pm
index c376c94..357a205 100644
--- a/lib/MusicBrainz/Server/Entity/Role/Linkable.pm
+++ b/lib/MusicBrainz/Server/Entity/Role/Linkable.pm
@@ -6,7 +6,7 @@ use List::UtilsBy qw( sort_by );
 
 has 'relationships' => (
     is => 'rw',
-    isa => 'ArrayRef[Relationship]',
+    isa => 'ArrayRef',
     default => sub { [] },
     lazy => 1,
     traits => [ 'Array' ],
@@ -25,12 +25,12 @@ sub grouped_relationships
 
     my %groups;
     my @relationships = sort {
-        my $a_sortname = $a->target->can('sort_name') ? 
-	   $a->target->sort_name :
-	   $a->target->name;
-        my $b_sortname = $b->target->can('sort_name') ? 
-	   $b->target->sort_name :
-	   $b->target->name;
+        my $a_sortname = $a->target->can('sort_name')
+            ? $a->target->sort_name
+            : $a->target->name;
+        my $b_sortname = $b->target->can('sort_name')
+            ? $b->target->sort_name
+            : $b->target->name;
         $a->link->begin_date        <=> $b->link->begin_date ||
         $a->link->end_date          <=> $b->link->end_date   ||
         $a->link->type->child_order <=> $b->link->type->child_order ||
diff --git a/lib/MusicBrainz/Server/NES.pm b/lib/MusicBrainz/Server/NES.pm
index 3226f08..9118ce9 100644
--- a/lib/MusicBrainz/Server/NES.pm
+++ b/lib/MusicBrainz/Server/NES.pm
@@ -63,6 +63,7 @@ sub with_transaction {
     return try {
         my $ret = $code->();
         $self->request('/close-session', {});
+        $self->clear_session_token;
 
         return $ret;
     }

commit f8a1554b758371d940896d5828c3a28add79745b
Author: Oliver Charles <[email protected]>
Date:   Fri Feb 1 12:02:14 2013 +0000

    Allow relating a work to a URL without attributes

diff --git a/lib/MusicBrainz/Server/Controller/Edit/Relationship.pm b/lib/MusicBrainz/Server/Controller/Edit/Relationship.pm
index 44da34c..dec899b 100644
--- a/lib/MusicBrainz/Server/Controller/Edit/Relationship.pm
+++ b/lib/MusicBrainz/Server/Controller/Edit/Relationship.pm
@@ -5,6 +5,8 @@ BEGIN { extends 'MusicBrainz::Server::Controller' };
 
 use MusicBrainz::Server::Constants qw( $EDIT_RELATIONSHIP_DELETE );
 use MusicBrainz::Server::Data::Utils qw( type_to_model );
+use MusicBrainz::Server::Entity::NES::Relationship;
+use MusicBrainz::Server::Entity::Tree::Work;
 use MusicBrainz::Server::Edit::Relationship::Delete;
 use MusicBrainz::Server::Edit::Relationship::Edit;
 use MusicBrainz::Server::Translation qw( l ln );
@@ -293,69 +295,86 @@ sub create_url : Local RequireAuth Edit
         $c->detach('/error_500');
     }
 
-    my $entity = $model->get_by_gid($gid);
-    unless (defined $entity) {
-        $c->stash( message => l('Entity not found') );
-        $c->detach('/error_404');
-    }
+    $c->model('MB')->with_nes_transaction(sub {
+        my $entity = $model->get_by_gid($gid);
+        unless (defined $entity) {
+            $c->stash( message => l('Entity not found') );
+            $c->detach('/error_404');
+        }
 
-    my $tree = $c->model('LinkType')->get_tree(@types);
-    my %type_info = build_type_info($tree);
+        my $tree = $c->model('LinkType')->get_tree(@types);
+        my %type_info = build_type_info($tree);
+
+        if (!%type_info) {
+            $c->stash(
+                template => 'edit/relationship/cannot_create.tt',
+                type0 => $types[0],
+                type1 => $types[1]
+            );
+            $c->detach;
+        }
 
-    if (!%type_info) {
         $c->stash(
-            template => 'edit/relationship/cannot_create.tt',
-            type0 => $types[0],
-            type1 => $types[1]
+            root      => $tree,
+            type_info => JSON->new->latin1->encode(\%type_info),
         );
-        $c->detach;
-    }
 
-    $c->stash(
-        root      => $tree,
-        type_info => JSON->new->latin1->encode(\%type_info),
-    );
+        my $attr_tree = $c->model('LinkAttributeType')->get_tree;
+        $c->stash( attr_tree => $attr_tree );
+        $self->attr_tree($attr_tree);
 
-    my $attr_tree = $c->model('LinkAttributeType')->get_tree;
-    $c->stash( attr_tree => $attr_tree );
-    $self->attr_tree($attr_tree);
-
-    my $form = $c->form(
-        form => 'Relationship::URL',
-        reverse => $types[0] eq 'url',
-        root => $tree,
-        attr_tree => $attr_tree
-    );
-
-    $c->stash(
-        entity => $entity,
-        type => $type,
-    );
+        my $form = $c->form(
+            form => 'Relationship::URL',
+            reverse => $types[0] eq 'url',
+            root => $tree,
+            attr_tree => $attr_tree
+        );
 
-    if ($c->form_posted && $form->submitted_and_valid($c->req->params)) {
-        my @attributes = $self->flatten_attributes($form->field('attrs'));
-        my $url = $c->model('URL')->find_or_insert($form->field('url')->value);
+        $c->stash(
+            entity => $entity,
+            type => $type,
+        );
 
-        my $e0 = $types[0] eq 'url' ? $url : $entity;
-        my $e1 = $types[1] eq 'url' ? $url : $entity;
+        if ($c->form_posted && $form->submitted_and_valid($c->req->params)) {
+            my $edit = $c->model('NES::Edit')->open;
+
+            my @attributes = $self->flatten_attributes($form->field('attrs'));
+            my $url = $c->model('NES::URL')->find_or_insert($edit, $c->user, $form->field('url')->value);
+
+            $c->model('NES::Work')->update(
+                $edit, $c->user, $entity,
+                MusicBrainz::Server::Entity::Tree::Work->new(
+                    relationships => [
+                        MusicBrainz::Server::Entity::NES::Relationship->new(
+                            link_type_id => $form->field('link_type_id')->value,
+                            target => $url
+                        )
+                    ]
+                )
+            );
 
-        $c->stash( url => $form->field('url')->value );
-        $c->model('MB')->with_transaction(sub {
-            $self->try_and_insert(
-                $c, $form,
-                @types,
-                entity0 => $e0,
-                entity1 => $e1,
-                link_type_id => $form->field('link_type_id')->value,
-                attributes => \@attributes,
-                ended => 0
-            ) or $self->detach_existing($c);
-        });
+            # my $e0 = $types[0] eq 'url' ? $url : $entity;
+            # my $e1 = $types[1] eq 'url' ? $url : $entity;
+
+            # $c->stash( url => $form->field('url')->value );
+            # $c->model('MB')->with_transaction(sub {
+            #     $self->try_and_insert(
+            #         $c, $form,
+            #         @types,
+            #         entity0 => $e0,
+            #         entity1 => $e1,
+            #         link_type_id => $form->field('link_type_id')->value,
+            #         attributes => \@attributes,
+            #         ended => 0
+            #     ) or $self->detach_existing($c);
+            # });
+
+            # my $redirect = $c->controller(type_to_model($type))->action_for('show');
+            # $c->response->redirect($c->uri_for_action($redirect, [ $gid ]));
+            # $c->detach;
 
-        my $redirect = $c->controller(type_to_model($type))->action_for('show');
-        $c->response->redirect($c->uri_for_action($redirect, [ $gid ]));
-        $c->detach;
-    }
+        }
+    });
 }
 
 sub delete : Local RequireAuth Edit
diff --git a/lib/MusicBrainz/Server/Controller/Role/Annotation.pm b/lib/MusicBrainz/Server/Controller/Role/Annotation.pm
index 3d566b2..57732d8 100644
--- a/lib/MusicBrainz/Server/Controller/Role/Annotation.pm
+++ b/lib/MusicBrainz/Server/Controller/Role/Annotation.pm
@@ -125,7 +125,7 @@ sub edit_annotation : Chained('load') PathPart RequireAuth Edit
         else
         {
             run_update_form(
-                $self, $c, $form,
+                $self->{model}, $c, $form,
                 build_tree => sub {
                     my $values = shift;
 
diff --git a/lib/MusicBrainz/Server/Data/NES/CoreEntity.pm b/lib/MusicBrainz/Server/Data/NES/CoreEntity.pm
new file mode 100644
index 0000000..9df8568
--- /dev/null
+++ b/lib/MusicBrainz/Server/Data/NES/CoreEntity.pm
@@ -0,0 +1,65 @@
+package MusicBrainz::Server::Data::NES::CoreEntity;
+use MooseX::Role::Parameterized;
+
+parameter 'root' => (
+    isa => 'Str',
+    required => 1
+);
+
+role {
+    my $params = shift;
+
+    requires 'tree_to_json', 'map_core_entity';
+
+    method create => sub {
+        my ($self, $edit, $editor, $tree) = @_;
+
+        my $response = $self->request($params->root . '/create', {
+            edit => $edit->id,
+            editor => $editor->id,
+            $self->tree_to_json($tree)
+        });
+
+        return $self->get_revision($response->{ref})
+    };
+
+    method update => sub {
+        my ($self, $edit, $editor, $base_revision, $tree) = @_;
+
+        die 'Need a base revision' unless $base_revision;
+
+        my $final_tree = $tree->complete
+            ? $tree
+            : $self->view_tree($base_revision)->merge($tree);
+
+        my $response = $self->request($params->root . '/update', {
+            edit => $edit->id,
+            editor => $editor->id,
+            revision => $base_revision->revision_id,
+            $self->tree_to_json($final_tree)
+        });
+
+        return undef;
+    };
+
+    method get_revision => sub {
+        my ($self, $revision_id) = @_;
+        return $self->_new_from_core_entity(
+            $self->request($params->root . '/view-revision', { revision => $revision_id }));
+    };
+
+    method get_by_gid => sub {
+        my ($self, $gid) = @_;
+        return $self->_new_from_core_entity(
+            $self->request('/work/find-latest', { mbid => $gid }))
+    };
+
+    method _new_from_core_entity => sub {
+        my ($self, $response) = @_;
+        return keys %$response == 0
+            ? undef
+            : $self->map_core_entity($response);
+    };
+};
+
+1;
diff --git a/lib/MusicBrainz/Server/Data/NES/URL.pm b/lib/MusicBrainz/Server/Data/NES/URL.pm
new file mode 100644
index 0000000..e8a8fad
--- /dev/null
+++ b/lib/MusicBrainz/Server/Data/NES/URL.pm
@@ -0,0 +1,44 @@
+package MusicBrainz::Server::Data::NES::URL;
+use Moose;
+use namespace::autoclean;
+
+use MusicBrainz::Server::Entity::Tree::URL;
+use MusicBrainz::Server::Entity::URL;
+
+with 'MusicBrainz::Server::Data::Role::NES';
+with 'MusicBrainz::Server::Data::NES::CoreEntity' => {
+    root => '/url'
+};
+
+sub find_or_insert {
+    my ($self, $edit, $editor, $url) = @_;
+    $self->create(
+        $edit, $editor,
+        MusicBrainz::Server::Entity::Tree::URL->new(
+            url => MusicBrainz::Server::Entity::URL->new(
+                url => $url
+            )
+        )
+    );
+}
+
+sub tree_to_json {
+    my ($self, $tree) = @_;
+
+    return (
+        url => $tree->url->url->as_string
+    );
+}
+
+sub map_core_entity {
+    my ($self, $response) = @_;
+    my %data = %{ $response->{data} };
+    return MusicBrainz::Server::Entity::URL->new(
+        url => $data{url},
+
+        gid => $response->{mbid},
+        revision_id => $response->{revision}
+    );
+}
+
+1;
diff --git a/lib/MusicBrainz/Server/Data/NES/Work.pm b/lib/MusicBrainz/Server/Data/NES/Work.pm
index 7205f63..9e5dbde 100644
--- a/lib/MusicBrainz/Server/Data/NES/Work.pm
+++ b/lib/MusicBrainz/Server/Data/NES/Work.pm
@@ -6,59 +6,19 @@ use MusicBrainz::Server::Entity::Work;
 use MusicBrainz::Server::WebService::Serializer::JSON::2::Utils qw( boolean );
 
 with 'MusicBrainz::Server::Data::Role::NES';
+with 'MusicBrainz::Server::Data::NES::CoreEntity' => {
+    root => '/work'
+};
 
-sub create {
-    my ($self, $edit, $editor, $tree) = @_;
+around create => sub {
+    my ($orig, $self, $edit, $editor, $tree) = @_;
 
     $tree->annotation('') unless $tree->annotation_set;
     $tree->aliases([]) unless $tree->aliases_set;
+    $tree->relationships([]) unless $tree->relationships_set;
 
-    my $response = $self->request('/work/create', {
-        edit => $edit->id,
-        editor => $editor->id,
-        _work_tree($tree)
-    });
-
-    return $self->get_revision($response->{ref});
-}
-
-sub update {
-    my ($self, $edit, $editor, $base_revision, $tree) = @_;
-
-    die 'Need a base revision' unless $base_revision;
-
-    my $final_tree = do {
-        if( $tree->work_set && $tree->aliases_set && $tree->iswcs_set ) {
-            $tree
-        }
-        else {
-            my $original_tree = $self->view_tree($base_revision);
-
-            $original_tree->work($tree->work)
-                if ($tree->work_set);
-
-            $original_tree->aliases($tree->aliases)
-                if ($tree->aliases_set);
-
-            $original_tree->iswcs($tree->iswcs)
-                if ($tree->iswcs_set);
-
-            $original_tree->annotation($tree->annotation)
-                if ($tree->annotation_set);
-
-            $original_tree;
-        }
-    };
-
-    my $response = $self->request('/work/update', {
-        edit => $edit->id,
-        editor => $editor->id,
-        revision => $base_revision->revision_id,
-        _work_tree($final_tree)
-    });
-
-    return undef;
-}
+    $self->$orig($edit, $editor, $tree);
+};
 
 sub view_tree {
     my ($self, $revision) = @_;
@@ -71,8 +31,8 @@ sub view_tree {
     );
 }
 
-sub _work_tree {
-    my $tree = shift;
+sub tree_to_json {
+    my ($self, $tree) = @_;
 
     return (
         work => do {
@@ -99,26 +59,22 @@ sub _work_tree {
                 type => $_->type_id,
                 locale => $_->locale
             }, @{ $tree->aliases }
-        ]
+        ],
+        relationships => {
+            url => [
+                map +{
+                    target => $_->target->gid,
+                    type => $_->link_type_id
+                }, grep {
+                    $_->target->isa('MusicBrainz::Server::Entity::URL')
+                } @{ $tree->relationships }
+            ]
+        }
     );
 }
 
-sub get_revision {
-    my ($self, $revision_id) = @_;
-    return _new_from_response(
-        $self->request('/work/view-revision', { revision => $revision_id }));
-}
-
-sub get_by_gid {
-    my ($self, $gid) = @_;
-    return _new_from_response(
-        $self->request('/work/find-latest', { mbid => $gid }))
-}
-
-sub _new_from_response {
-    my ($response) = @_;
-    return undef if keys %$response == 0;
-
+sub map_core_entity {
+    my ($self, $response) = @_;
     my %data = %{ $response->{data} };
     return MusicBrainz::Server::Entity::Work->new(
         name => $data{name},
diff --git a/lib/MusicBrainz/Server/Data/Utils.pm b/lib/MusicBrainz/Server/Data/Utils.pm
index f02b703..308b121 100644
--- a/lib/MusicBrainz/Server/Data/Utils.pm
+++ b/lib/MusicBrainz/Server/Data/Utils.pm
@@ -63,7 +63,7 @@ Readonly my %TYPE_TO_MODEL => (
     'release'       => 'Release',
     'release_group' => 'ReleaseGroup',
     'url'           => 'URL',
-    'work'          => 'Work',
+    'work'          => 'NES::Work',
 );
 
 sub copy_escape {
diff --git a/lib/MusicBrainz/Server/Entity/NES/Edit.pm b/lib/MusicBrainz/Server/Entity/NES/Edit.pm
new file mode 100644
index 0000000..465e4dc
--- /dev/null
+++ b/lib/MusicBrainz/Server/Entity/NES/Edit.pm
@@ -0,0 +1,8 @@
+package MusicBrainz::Server::Entity::NES::Edit;
+use Moose;
+
+has id => (
+    is => 'ro',
+);
+
+1;
diff --git a/lib/MusicBrainz/Server/Entity/NES/Relationship.pm b/lib/MusicBrainz/Server/Entity/NES/Relationship.pm
new file mode 100644
index 0000000..a16a8a2
--- /dev/null
+++ b/lib/MusicBrainz/Server/Entity/NES/Relationship.pm
@@ -0,0 +1,14 @@
+package MusicBrainz::Server::Entity::NES::Relationship;
+use Moose;
+
+has target => (
+    is => 'ro',
+    required => 1
+);
+
+has link_type_id => (
+    is => 'ro',
+    required => 1
+);
+
+1;
diff --git a/lib/MusicBrainz/Server/Entity/Tree/URL.pm b/lib/MusicBrainz/Server/Entity/Tree/URL.pm
new file mode 100644
index 0000000..87a6836
--- /dev/null
+++ b/lib/MusicBrainz/Server/Entity/Tree/URL.pm
@@ -0,0 +1,22 @@
+package MusicBrainz::Server::Entity::Tree::URL;
+use Moose;
+
+has url => (
+    is => 'rw',
+    predicate => 'url_set',
+);
+
+sub merge {
+    my ($self, $tree) = @_;
+
+    die 'Undefined';
+
+    return $self;
+}
+
+sub complete {
+    my $tree = shift;
+    die 'Undefined';
+}
+
+1;
diff --git a/lib/MusicBrainz/Server/Entity/Tree/Work.pm b/lib/MusicBrainz/Server/Entity/Tree/Work.pm
index 8d4c4f7..d71270e 100644
--- a/lib/MusicBrainz/Server/Entity/Tree/Work.pm
+++ b/lib/MusicBrainz/Server/Entity/Tree/Work.pm
@@ -21,4 +21,35 @@ has annotation => (
     predicate => 'annotation_set'
 );
 
+has relationships => (
+    is => 'rw',
+    predicate => 'relationships_set'
+);
+
+sub merge {
+    my ($self, $tree) = @_;
+
+    $self->work($tree->work)
+        if ($tree->work_set);
+
+    $self->aliases($tree->aliases)
+        if ($tree->aliases_set);
+
+    $self->iswcs($tree->iswcs)
+        if ($tree->iswcs_set);
+
+    $self->annotation($tree->annotation)
+        if ($tree->annotation_set);
+
+    $self->relationships($tree->relationships)
+        if ($tree->relationships_set);
+
+    return $self;
+}
+
+sub complete {
+    my $tree = shift;
+    return $tree->work_set && $tree->aliases_set && $tree->iswcs_set;
+}
+
 1;
diff --git a/lib/MusicBrainz/Server/Model/MB.pm b/lib/MusicBrainz/Server/Model/MB.pm
index db9f34a..3a90e48 100644
--- a/lib/MusicBrainz/Server/Model/MB.pm
+++ b/lib/MusicBrainz/Server/Model/MB.pm
@@ -6,6 +6,7 @@ extends 'Catalyst::Model';
 use DBDefs;
 use Module::Pluggable::Object;
 use MusicBrainz::Server::Context;
+use Moose::Util qw( find_meta );
 
 has 'context' => (
     isa        => 'MusicBrainz::Server::Context',
@@ -56,7 +57,9 @@ sub models {
     );
 
     for my $model (sort $searcher->plugins) {
-        next if $model =~ /Data::Role/;
+        Class::MOP::load_class($model);
+        my $meta = find_meta($model);
+        next if $model =~ /Data::Role/ || !defined($meta) || $meta->isa('Moose::Meta::Role');
         my ($model_name) = ($model =~ m/.*::Data::(.*)/);
         $model =~ s/^MusicBrainz::Server::Data:://;
 
diff --git a/lib/MusicBrainz/Server/NES.pm b/lib/MusicBrainz/Server/NES.pm
index 80d8401..3226f08 100644
--- a/lib/MusicBrainz/Server/NES.pm
+++ b/lib/MusicBrainz/Server/NES.pm
@@ -61,11 +61,16 @@ sub with_transaction {
     $self->session_token($self->request('/open-session', {})->{token});
 
     return try {
-        return $code->();
+        my $ret = $code->();
+        $self->request('/close-session', {});
+
+        return $ret;
     }
-    finally {
+    catch {
         try { $self->request('/close-session', {}) };
         $self->clear_session_token;
+
+        die $_;
     };
 }
 
diff --git a/lib/MusicBrainz/Server/NES/Controller/Utils.pm b/lib/MusicBrainz/Server/NES/Controller/Utils.pm
index 6f295c7..f7aa71c 100644
--- a/lib/MusicBrainz/Server/NES/Controller/Utils.pm
+++ b/lib/MusicBrainz/Server/NES/Controller/Utils.pm
@@ -38,17 +38,17 @@ sub run_edit_form {
 }
 
 sub run_update_form {
-    my ($controller, $c, $form, %opts) = @_;
+    my ($model, $c, $form, %opts) = @_;
 
     run_edit_form(
         $c, $form,
         on_post => sub {
             my ($values, $edit) = @_;
 
-            my $revision = $c->model( $controller->{model} )->get_revision(
+            my $revision = $c->model( $model )->get_revision(
                 $values->{revision_id});
 
-            $c->model( $controller->{model} )->update(
+            $c->model( $model )->update(
                 $edit, $c->user, $revision,
                 $opts{build_tree}->($values, $revision)
             );
@@ -104,7 +104,7 @@ sub create_update {
         %opts,
         callback => sub {
             my $form = shift;
-            run_update_form($controller, $c, $form, %opts);
+            run_update_form($controller->{model}, $c, $form, %opts);
         }
     );
 }
diff --git a/root/components/common-macros.tt b/root/components/common-macros.tt
index ee84d23..a26cf6f 100644
--- a/root/components/common-macros.tt
+++ b/root/components/common-macros.tt
@@ -170,6 +170,8 @@ END -%]
             l('release groups');
         CASE 'editor';
             l('editors');
+        CASE 'url';
+            l('urls');
         CASE 'work';
             l('works');
         CASE DEFAULT;

-----------------------------------------------------------------------


hooks/post-receive
-- 
mb_server