A Foolish Manifesto

fREWdiculous!

I am proud to announce a new release of DBIx::Class::Helpers. There are five major changes in this release.

First off, the latest release adds DBIx::Class::Candy exports. So if you are using DBIx::Class::Candy to define a result, certain methods will be imported into your namespace. For example, DBIx::Class::Helper::Row::SubClass will export a subclass subroutine into your module. Not huge but nice nonetheless.

Next up, we have four shiny new components. Two are ResultSet components and two are Result components. One of the two ResultSet components was originally going to be in the core of DBIx::Class, but I decided to make a helper first to ensure that we iron out the details before we release it in core. That component is DBIx::Class::Helper::ResultSet::RemoveColumns. It does exactly what it sounds like. With it you can do

1
2
3
$resultset->search(undef, {
   remove_columns => ['id']
})

and the id column will no longer be selected in your ResultSet. I am sure that it has some quirks, but I am not sure what they would be till people use this. So have at it!

The next component, DBIx::Class::Helper::ResultSet::AutoRemoveColumns, is based upon RemoveColumns. Again, the name should make it clear what it does. Currently it removes typically large columns by default, like text, blob, and the like. See the docs for exactly what it removes. (Note: later on I hope to add a component that adds lazy columns as detailed in ovid’s post here. Ovid if you are reading this I can’t comment on your blog.)

Next up is a fairly simple component, DBIx::Class::Helper::Row::StorageValues. It gives you access to the last known stored value of a column. For example:

1
2
3
4
5
my $foo = $resultset->search({ name => 'frew'})
   ->next;
$foo->name('frioux');
# prints "frew"
say $foo->get_storage_value('name');

Building upon that we have my favorite new component: DBIx::Class::Helper::Row::OnColumnChange. This module adds powerful hooks for calling methods when a column has been modified. If you enable StorageValues for the column you hook into you get to look at the old value and the new value, which is pretty cool. There are three hooks: before_column_change, around_column_change, and after_column_change. It automatically takes into account values changing because of accessors as well as by the arguments passed to update. Also note that it allows you to tell it to wrap the call to update and the column change method in a transaction so that you can safely do things to other tables in the method. Anyway, enough talk, here’s a small example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__PACKAGE__->add_column(relationship_status => {
   data_type               => 'varchar',
   length                  => 30,
   keep_storage_value      => 1,
});

__PACKAGE->before_column_change(relationship_status => {
   method    => 'happy_times',
   txn_wrap  => 1,
});

sub happy_times {
   my ($self, $old, $new) = @_;
   $self->significant_other->update({ feelings => 'happy' })
      if $new eq 'together' && $old eq 'apart'
}

So basically if relationship status changes from apart to togehter the significant other gets marked as happy, and all of this is done in a transaction, which is pretty awesome.

Anyway, hopefully this makes your job easier. Have a good Friday!

  • 1 Comment
  • Filed under: Uncategorized
  • Announcing DBIx::Class::Candy

    Over a year ago I read this blog post. To be honest at the time I thought it was mostly silly and I still feel that way. The things that are important to me in an ORM are capabilities, not subjective prettiness of code. But, I also get tired of typing repetitive things, especially __PACKAGE__->. That’s just too many shift keys! So after working on a few different modules and accruing various bits of knowledge here and there I learned what I needed to to create a sugar layer for DBIx::Class that doesn’t throw the baby out with the bath-water.


    I am proud to announce the initial, development version of DBIx::Class::Candy, which should be coming to a CPAN mirror near you very soon. If you just can’t wait, use cpanf to get it right now. The basic gist of it is that you can use:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
     package MyApp::Schema::Result::Artist;

     use DBIx::Class::Candy;

     table 'artists';

     column id => {
       data_type => 'int',
       is_auto_increment => 1,
     };

     column name => {
       data_type => 'varchar',
       size => 25,
       is_nullable => 1,
     };

     primary_key 'id';

     has_many albums => 'A::Schema::Result::Album', 'artist_id';

     1;

    instead of

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
     package MyApp::Schema::Result::Artist;

     use strict;
     use warnings;
     use base 'DBIx::Class::Core';

     __PACKAGE__->table('artists');

     __PACKAGE__->add_columns(
       id => {
         data_type => 'int',
         is_auto_increment => 1,
       },
       name => {
         data_type => 'varchar',
         size => 25,
         is_nullable => 1,
       }
     );

     __PACKAGE__->set_primary_key('id');

     __PACKAGE__->has_many( albums => 'A::Schema::Result::Album', 'artist_id' );

     1;

    There are a few other features, like having it turn on 5.10 or 5.12 features, use a non standard base, and more. Check it out now!

  • 5 Comments
  • Filed under: Uncategorized
  • Being a Speaker at YAPC 2010

    This year Rob Kinyon and mst convinced me to do some speaking at YAPC. I ended up doing three forty minute talks. The DBIx::Class one was certainly the easiest, but also the one I was least invested in. I didn’t write DBIx::Class and it’s a big enough project that the slides nearly wrote themselves.

    I also did a talk on DBIx::Class::DeploymentHandler. I am a little frustrated with how this talk went down. I am confident that DBICDH is an excellent piece of software that can do some really cool things and I kinda botched the end of the talk (very abrupt.) I wouldn’t call the talk a failure; it had a great turnout and at the very least I got to discuss features with jnapiorkowski, which was fun.

    My third talk was about DBIx::Exceptions. It was hard for a couple reasons; first off DBIE isn’t actually done yet. That certainly is not conducive to talking about something. Second, what it ultimately does really isn’t that complicated. The module is almost entirely a function of doing some research. So while I think that the subject is good and interesting, I don’t think it warrants a whole 40 minute talk. I actually had a long discussion with Rob about exceptions in general and he strongly supported the point of view that exceptions in any language are fundamentally flawed. I’ll turn that into a blog post at some point.

    I finished all three of the talks in less than forty minutes; I think that was a combination of me talking too fast (DBIC + DBICDH) and not actually having enough content (DBIE.) I would have done better were I less nervous, which should be less and less of a problem as a I practice technical speaking more. I actually did a lot of presentations in college and rarely had these issues, but in college I almost never spoke about software I wrote and my talks were usually from 10 to 20 minutes long. Another thing that I feel is important is that I need to try to add humor to my talks. The talks that are the most engaging are also the ones that are entertaining. I’m not sure I how I could work in jokes etc, but I need to try to do that next time.

    In other news, I’m pretty worn out in general from YAPC. I just need a break. So while I will do bug fixes and whatnot for my existing modules, and I still hope to release DBIx::Exceptions very soon, I’m going to try to chill out for about a month before taking on any of the other large ideas I have in mind for the future. Or at least I won’t start them on purpose :-)

  • 0 Comments
  • Filed under: Uncategorized
  • Announcing DBIx::Class::DeploymentHandler

    Do you remember when you first realized that you were not the only person with a perspective in the world? I do. I was 5ish and I remember looking into the car to the left of me and seeing another person looking at me from their respective car. I remember thinking, “This is not what it is like from their point of view.” I distinctly remember reevaluating things all day that day. I am sure that I was still just as selfish and childish as I was before that moment, but it certainly changed my point of view.


    I am proud to announce, after three months of work, that DBIx::Class::DeploymentHandler is at a point where I’d call it stable and usable. DBICDH is a much more flexible replacement for DBIx::Class::Schema::Versioned. Castaway did a great job with making Schema::Versioned, and without it there is no way I would have gotten started on DBICDH, but it is my sincere hope that this will be the recommended tool instead of Schema::Versioned from now on. Rob Kinyon and mst had significant influence on the overall API and design, so it is at least influenced by Very Smart people. ribasushi helped a lot later on by pointing out poorly named methods and directories as well as helping me use SQL::Translator correctly since he knows all of its weaknesses and strengths.

    Major features this has over DBIx::Class::Schema::Versioned:

    • Multiple files for migrations
    • Perl files in migrations
    • Shared Perl/SQL for different databases
    • Downgrades
    • Not to mention extreme customizability

    So try it out today! I am looking forward to getting bug reports soon :-)

    Oh also, if it does not quite do what you want…

    PATCHES WELCOME! :-D

  • 1 Comment
  • Filed under: Uncategorized
  • DBIx::Class has migrated to git!

    Woohoo! git!

    I am so happy to announce that DBIx::Class has migrated to git!

    If people latch on well, this should benefit is in a number of ways. The first thing is that most people should appreciate is the ability to check in to source control without needing to commit to the remote repository. Not only does this make things way faster, it also means that you can work sanely offline. A lot of people did this before with SVK, but SVK is slow and a hassle (I think) to install and setup.

    Another thing that this should help is our history. I don’t mean to point out a specific person, I did this all the time with svn (it’s just the nature of the beast,) but take a look at some of these commit messages. The SVK merge messages are pretty obnoxious, but that’s not what I’m referring to. The ones that bother me are the: “Oops,” “Typo,” “Thinko,” etc. With git those can be cleanly squashed into another commit. In fact, I would recommend cleaning up your history before you push every time. This is how I do that:

    1
    git rebase --root --onto master --interactive

    That will rebase your current branch (all of it) onto master, and give you an editor that will let you fix commit messages, merge commits, and more. If people do not do this I fully intend to do it myself before I merge a branch in, if I do. And that brings me to the next point which we shall discuss.

    Workflow

    We toyed with the idea of setting up git such that everyone would just use github or gitorious or whatever and ribasushi and I would be in charge of merging remote branches into the DBIC master, but mst veto’d the idea in favor of a more communal approach. Basically the workflow we will use looks like the following:

    1. Get a commitbit (liberal policy)
    2. Clone the repo (dbsrgits@git.shadowcat.co.uk:DBIx-Class.git)
    3. Make a topic branch based on what you are doing, do work on it, whatever
    4. Push branch to repo
    5. Ask a fellow DBIC’er to review the branch, that includes me, ribasushi, Caelum, and really anyone else with a commitbit
    6. If the reviewer thinks everything is sane, the reviewer will merge the branch in; to be a bit more clear, you should not merge your own branch into master, that is the reviewer’s job
    7. Delete your branch locally and remotely after everything is merged in, and work on other stuff!

    A note about merges, don’t merge master into your branch. It makes for yucky history. Instead, rebase your branch onto master. Currently the thought is that anything except for master can be rebased. Of course if you and another dev are working in the branch you might want to keep that to a minimum, but at the very least you should be rebasing to squash silly commits before you push, and then when you do the final merge into master you should rebase first, so that history remains sane. Of course, the best time to rebase to fix silly history is before you push, and the best time to rebase to make it so that you are fastforwarding master is right before the final merge, so try to only do that then.

    At some point I plan on writing a DBIx::Class::Manual::Contributing in the spirit of Moose::Manual::Contributing, but ours will be significantly more lax. In the meantime, just swing by #dbix-class, get your commitbit, and help out!

  • 0 Comments
  • Filed under: Uncategorized
  • New DBIx::Class::Journal!

    I’m proud to announce a new version of DBIx::Class::Journal after almost three years of different people working on different parts!

    It’s certainly not complete. The main issues for me are:

    1. It only versions tables with single column PK’s
    2. It has no simple way to have related data in the journal

    The former is a SMOP, the latter, on the other hand, is a very serious architectural issue which I don’t think can even safely be solved. It *might* be as simple as just replicating all of the relationships in the original result and then adding in another column to the relationship which points to a version of that result. Or it might not. I need to consider it and look at things, but I think it can be done.

    Honestly, if I had all the time in the world, I’d rewrite ::Journal from the ground up and make it a lot more malleable. Unfortunately I don’t have all the time in the world and personally I don’t have much use for it. Parts of my job do, but they only pay so much for features :-)

    Anyway, enjoy it! It should be pretty solid. Just make sure you read the limitations before use.

  • 2 Comments
  • Filed under: Uncategorized
  • New DBIx::Class::Journal

    We are planning on using DBIx::Class::Journal in a couple different projects of ours. Unfortunately DBICJ has foundered for more than a year as a dev release, and even that won’t test correctly on win32. So I’ve done some work on it to get it in a more releasable state. I just released a developer release to CPAN and if you currently use DBICJ please test it out. Even if you don’t use it at least test it so I can see if I get failures. The release should end up here, but it will take a couple hours.

  • 1 Comment
  • Filed under: Uncategorized
  • New stuff in DBIx::Class::Helpers 2.00200

    A new release of the resplendent Perl ORM DBIx::Class means new release of DBIx::Class::Helpers

    The ResultSet::Random helper had the wrong function used for MySQL. That was fixed thanks to an RT from pldoh.

    get_namespace_parts from the util package was unnecessarily strict. Thanks to melo for the prodding to do that.

    I refactored some of the code in core DBIx::Class so that I can more easily detect is_numeric with Row::NumifyGet, instead of requiring the user to specify it. Normally DBIx::Class autodetects it based on column type, but that code wasn’t quite generic enough until now. Nice!

    And then the most exciting bit is a new helper entirely for the suite: Row::ToJSON. Basically I was sick of doing this:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package ACD::Schema::Result::Foo;

    # regular package stuff here

    sub TO_JSON {
      my $self = shift;
      return {
        id => $self->id,
        foo => $self->foo,
        # etc etc ad nausium
      }
    }

    "distraction";

    Of course that can be shortened to:

    1
    2
    3
    4
    sub TO_JSON {
      my $self = shift;
      return { map +( $_ => $self->$_), qw{id foo ...} }
    }

    But I still have to make that stupid columns list! This shiny new helper makes a TO_JSON method that will simply include all of your columns except for the “heavy” ones like TEXT, NTEXT, or BLOB. Of course you can have finer-grained control than that by explicitly saying to include (or not) a column in it’s configuration. See the docs for all the nitty gritty details.

  • 0 Comments
  • Filed under: Uncategorized
  • Do Passwords Right

    You all know not to put your passwords into the database in plaintext. Catalyst and DBIx::Class::EncodedColumn make doing this super easy and completely secure.

    First off, you might want to check out the wikipedia article about cryptographic hash functions. The gist of it though is this: a password stored in plain text is obviously compromised if the passwords file gets into the hands of evildoers. You can “hash” the passwords and they are now harder for the attackers to transform into plain-text. If your password is good it is nearly impossible, but basically what can happen is that the attacker uses the algorithm to generate hashes for every word in the dictionary or whatever and now they basically can crack all the basic passwords.

    You can take it a step further and “salt” your passwords (wikipedia salt article.) A simple way of doing that is just to concatenate some string onto the end of all of your passwords. This will make dictionary attempts useless unless they know your salt. Typically when using a salt the salt is kept secret.

    And then you can have a unique salt per password. Imagine a scheme where the salt is $username$id. It would require the attackers to basically generate a dictionary per user!

    The scheme we’ve settled on uses Eksblowfish. The DBIC Component for it actually uses a 16 character randomly generated salt for every password. Nice!

    Ok, so how does one apply such sweet, sweet code? First, (always) set up your model. This is a slightly trimmed version of ours:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    package MTSI::Schema::Result::User;

    use strict;
    use warnings;

    use parent 'DBIx::Class::Core';

    use CLASS;

    CLASS->load_components(qw/EncodedColumn/);

    CLASS->table('users');

    CLASS->add_columns(
       id => {
          data_type         => 'integer',
          is_numeric        => 1,
          is_nullable       => 0,
          is_auto_increment => 1,
       },
       username => {
          data_type         => 'varchar',
          size              => 50,
          is_nullable       => 0,
       },
       password => {
          data_type     => 'CHAR',
          size          => 59,
          encode_column => 1,
          encode_class  => 'Crypt::Eksblowfish::Bcrypt',
          encode_args   => { key_nul => 0, cost => 8 },
          encode_check_method => 'check_password',
    });

    CLASS->set_primary_key('id');

    1;

    Easy peasy! The encode_check_method option for password basically puts a method in your result class that you can call with a plaintext password and it returns true or false if the password is legitimate. The nice thing about that is that if you decide to switch to some other kind of hashing, your controller stays the same. Model code for the win!

    Next up, the Catalyst configuration. This was what took me a while to find, but thanks to mst I finally found it yesterday. The package we use for auth is the same one everyone uses in Cat: Catalyst::Plugin::Authentication. The docs that I was looking for specifically were the ones for Catalyst::Authentication::Credential::Password. So after reading those docs, the following is the catalyst config snippet one would use for these nice passwords:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
       'Plugin::Authentication' => {
          use_session => 1,
          default => {
             credential => {
                class              => 'Password',
                password_type      => 'self_check',
             },
             store => {
                class                     => 'DBIx::Class',
                user_model                => 'DB::User', #<-- DB refers to the name of the
                role_relation             => 'roles',         #     model class we are using
                use_userdata_from_session => 1,
             }
          }
       },

    Note: the password_type of self_check is what tells the controller to just call $result->check_password($plaintext).

    So there you have it. That’s all the code you need for secure passwords with Catalyst. If you make a new project and your users passwords get compromised it is your fault.

    Have a nice day :-)

  • 3 Comments
  • Filed under: Uncategorized
  • An Exposition on Specific Time Saving Code

    I write a lot of ExtJS grids at work. I have written JavaScript classes for our Ext grids that generate as much as possible automatically, but the actual column definitions of the grids are almost always unique. The project I am on now is nearing our first real deploy, and we’re late, so things have been really, really busy.

    It wasn’t until recently that I realized just how much time I spent working on grids and their related records (representation of the rows of a grid.) Although even if I’d known just how much I do this at the beginning of the project, I certainly didn’t know DBIx::Class as much as I do now, in addition to the other 4 non-core modules that I’ll mention.

    Because I’ve been working all these super long hours (12-14 a day), often after a frustrating day I’ll try to code something more relaxing and rewarding at home or at work but not during work. So today I decided to finally take the plunge and do what I’ve been pondering for a few months and write some code to generate Ext scaffolding for me. So I’m going to walk you through the script that I wrote (and will probably work on more as time goes by.)

    This isn’t a full module, not even in my repo yet, so it’s all just in one file. Here’s the boilerplate intro:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #!perl

    use strict;
    use warnings;

    use feature ':5.10';

    use Syntax::Keyword::Gather;
    use String::CamelCase ();
    use Lingua::EN::Inflect ();
    use List::Util;
    use Statistics::Basic 'mean';

    That’s pretty basic. We use a bunch of modules that I’ve used in at least 1 other project before and was happy with.

    1
    2
    3
    4
    5
    6
    7
    8
    use FindBin;
    use lib "$FindBin::Bin/../local/lib", "$FindBin::Bin/../lib";
    use ACD::Schema;
    use Config::JFDI;
    my $config = Config::JFDI->new(name => 'acd', path => "$FindBin::Bin/../acd");
    my $config_hash = $config->get;
    my $connect_info = $config_hash->{Model}{DB}{connect_info};
    my $schema = ACD::Schema->connect($connect_info);

    That’s the code to parse any kind of Catalyst config file and the grab a new schema based on it. It’s a ton of biolerplate but I live with it.

    So next up is a basic inflection function that gives us all the different forms of a word we might need. It starts with either “single_foo” or “SingleBar” and gives us six variations based on that:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    sub inflect {
       my $word = shift;

       my $return = { singular => {}, plural => {}};
       if ( defined $word->{camel}) {
          $word = $word->{camel};
          $return->{singular}{camel} = $word;
          $return->{singular}{noncamel} = String::CamelCase::decamelize($word);
       } else {
          $word = $word->{noncamel};
          $return->{singular}{camel} = String::CamelCase::camelize($word),
          $return->{singular}{noncamel} = $word;
       }
       $return->{singular}{human} = join(
          q{ }, map ucfirst $_,
          split /_/, $return->{singular}{noncamel}
       );

       $return->{plural}{noncamel} = join( q{_}, split / /,
          Lingua::EN::Inflect::PL(
             join q{ },
             split /_/,
             $return->{singular}{noncamel}
          )
       );
       $return->{plural}{camel} = String::CamelCase::camelize(
          $return->{plural}{noncamel}
       );
       $return->{plural}{human} = join( q{ }, map ucfirst $_, split /_/,
          $return->{plural}{noncamel}
       );
       return $return
    }

    So that uses String::CamelCase and Lingua::EN::Inflect combined with join and split mostly.

    We’re *almost* ready to generate a record. But first we need to define a mapping from the data type in the database to the data type that Ext uses:

    1
    2
    3
    4
    5
    6
    7
    8
    my $types_xform = {
       int => 'int',
       float => 'float',
       varchar => 'string',
       bit => 'boolean',
       datetime => 'date',
       money => 'float',
    };

    Ok, now let’s look at the code to generate an Ext.record:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    sub generate_record {
       my $schema = shift;
       my $source_name = shift;

       my $source = $schema->source($source_name);

       return qq{Ext.ns('ACDRI.record');

    ACDRI.record.$source_name = Ext.data.Record.create([\n} .
       join(qq{,\n}, sort { $a cmp $b } gather {
          for (map [$_, $source->column_info($_)], $source->columns) {
             my ($column, $info) = @{$_};
             take "   {name: '$column', type: '$types_xform->{$info->{data_type}}'}";
          }
       }) .  "\n]);";

    }

    So what’s going on here is that we get the source from the schema. The source could be considered something like a table definition, although it can also point at a view or whatever too. Then we start generating the string representing our record, and then we use a join/gather combo to get the column data the way we want it.

    We could certainly just use a more complex map instead of the for+gather that we have, but I personally feel than any map where you *must* use the block form is cumbersome. So we join together all of the strings that gather took, and then append the end of the definition, and voilà, we have a record!

    Here’s example output on our Customer source:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Ext.ns('ACDRI.record');

    ACDRI.record.Customer = Ext.data.Record.create([
       {name: 'color_code', type: 'string'},
       {name: 'comments', type: 'string'},
       {name: 'id', type: 'string'},
       {name: 'last_edited_date', type: 'date'},
       {name: 'name', type: 'string'},
       {name: 'price_approval_required', type: 'boolean'},
       {name: 'sales_representative_id', type: 'int'}
    ]);

    If you’re still following along you should be fine with the next, much more impressive functionality.

    I’d like to have the script do as much as possible to speed up my work, so let’s see how far we can take this. First, I made a mapping from type to renderer, so that things that are datetimes use our custom datetime renderer, same with booleans:

    1
    2
    3
    4
    my $renderer_from_type = {
       datetime => 'ACDRI.fn.Renderers.dateTime',
       bit => 'ACDRI.fn.Renderers.bool',
    };

    Next, I wrote a function that would try it’s best to guess how wide a column should be based on it’s header and the average width of the strings inside of it:

    1
    2
    3
    4
    5
    6
    7
    sub avg_width {
       my ($rs, $col, $header) = @_;
       List::Util::max (
          mean(map +( defined($_) ? length "$_" : 0 ), $rs->get_column($col)->all), # average width of field
          length $header                                                            # col header
       );
    }

    Now finally we have the grid function:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    sub generate_grid {
       my $schema = shift;
       my $source_name = shift;

       my $source_names = inflect({ camel => $source_name });
       my $source = $schema->source($source_name);

       return qq`/*global Ext */
    /*global ACDRI */
    /*global MTSI */
    Ext.ns('ACDRI.ui.grid');

    /**
     * \@class ACDRI.ui.grid.$source_names->{plural}{camel}
     * \@extends MTSI.ui.Grid
     */
    ACDRI.ui.grid.$source_names->{plural}{camel} = Ext.extend(MTSI.ui.Grid, {
       title: '$source_names->{plural}{human}',
       record: ACDRI.record.$source_names->{singular}{camel},
       updateConfig: {
          xtype: 'ACDRI.ui.form.$source_names->{singular}{camel}'
       },
       addConfig: {
          xtype: 'ACDRI.ui.form.$source_names->{singular}{camel}',
       },
       initComponent: function () {
          //this.sortInfo = {
          //   field: 'part_id',
          //   direction: 'asc'
          //};
          var config = {
             url: '$source_names->{plural}{noncamel}',
             itemName: '$source_names->{singular}{human}',
             columns: [`
    .
       join (qq{, }, gather {
          for (map [$_, $source->column_info($_)], $source->columns) {
             my ($column, $info) = @{$_};
             my $colnames = inflect({ noncamel => $column });
             my $renderer = $renderer_from_type->{$info->{data_type}}
                ? "\n            renderer: $renderer_from_type->{$info->{data_type}},"
                : '';
             my $width;
             given ($info->{data_type}) {
                when ('varchar') {
                   $width = int 7*avg_width(
                      $source->resultset, $column, $colnames->{singular}{human}
                   );
                }
                when ('datetime') {
                   $width = List::Util::max(
                      int 7 * length $colnames->{singular}{human},
                      57
                   );
                }
                default {
                   $width = int 7 * length $colnames->{singular}{human};
                }
             }
             take qq[{
                header: '$colnames->{singular}{human}',
                dataIndex: '$column',
                sortable: true,
                hidden: false,$renderer
                width: $width
             }];
          }
       }) .  qq`]
          };

          Ext.apply(this, Ext.apply(this.initialConfig, config));
          ACDRI.ui.grid.$source_names->{plural}{camel}.superclass.initComponent.apply(this, arguments);
       }
    });

    Ext.reg('ACDRI.ui.grid.$source_names->{plural}{camel}', ACDRI.ui.grid.$source_names->{plural}{camel});
    `
    ;
    }

    We see nearly the same structure here that we did with the record, except much more intricate. Notice the code to generate the width uses the magic constant 7. As I played with this I found that 7 seemed to work for the font that Ext uses by default (Arial?) Optimally I would actually use some kind of metrics package to ask it for the width of all of the strings that I generated in the function above and average that, instead of averaging character lengths. But this seems to work really, really well, so the ROI is pretty good.

    Also note the code to pick and insert the renderer. It’s not complex or anything, but it yields very convenient results. Here’s the output of running this one on the same source we used for the record:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    /*global Ext */
    /*global ACDRI */
    /*global MTSI */
    Ext.ns('ACDRI.ui.grid');

    /**
     * @class ACDRI.ui.grid.Customers
     * @extends MTSI.ui.Grid
     */

    ACDRI.ui.grid.Customers = Ext.extend(MTSI.ui.Grid, {
       title: 'Customers',
       record: ACDRI.record.Customer,
       updateConfig: {
          xtype: 'ACDRI.ui.form.Customer'
       },
       addConfig: {
          xtype: 'ACDRI.ui.form.Customer',
       },
       initComponent: function () {
          //this.sortInfo = {
          //   field: 'part_id',
          //   direction: 'asc'
          //};
          var config = {
             url: 'customers',
             itemName: 'Customer',
             columns: [{
                header: 'Comments',
                dataIndex: 'comments',
                sortable: true,
                hidden: false,
                width: 226
             }, {
                header: 'Color Code',
                dataIndex: 'color_code',
                sortable: true,
                hidden: false,
                width: 70
             }, {
                header: 'Id',
                dataIndex: 'id',
                sortable: true,
                hidden: false,
                width: 35
             }, {
                header: 'Last Edited Date',
                dataIndex: 'last_edited_date',
                sortable: true,
                hidden: false,
                renderer: ACDRI.fn.Renderers.dateTime,
                width: 112
             }, {
                header: 'Name',
                dataIndex: 'name',
                sortable: true,
                hidden: false,
                width: 136
             }, {
                header: 'Price Approval Required',
                dataIndex: 'price_approval_required',
                sortable: true,
                hidden: false,
                renderer: ACDRI.fn.Renderers.bool,
                width: 161
             }, {
                header: 'Sales Representative Id',
                dataIndex: 'sales_representative_id',
                sortable: true,
                hidden: false,
                width: 161
             }]
          };

          Ext.apply(this, Ext.apply(this.initialConfig, config));
          ACDRI.ui.grid.Customers.superclass.initComponent.apply(this, arguments);
       }
    });

    Ext.reg('ACDRI.ui.grid.Customers', ACDRI.ui.grid.Customers);

    Note that there are probably some things that we could do a little better. For instance, we should probably check for text fields and not include them in the grid. We could also be clever and look for anything ending with _id and have it hidden by default. But in general, the above is very nice results. At the very least you are probably going to need to reorder the columns (although you could give some kind of grid_order_id to the column definition in the Schema::Result code, but I don’t think that’s worth it.)

    So I hope someone else enjoyed reading this as much as I did writing it. After I’ve exhausted all of the little Ext things I can use this for (form, etc) I will probably look into messing with DBICx::AutoDoc to add in Dia outputs.

  • 2 Comments
  • Filed under: Uncategorized