diff --git a/docs/Parliament.pod b/docs/Parliament.pod index c46874ad..1479140f 100644 --- a/docs/Parliament.pod +++ b/docs/Parliament.pod @@ -4,4 +4,73 @@ Parliament is accessible via the URL C. The list of methods below represents changes and additions to the methods that all L share. + +=head2 view_propositions (session_id, building_id ) + +Returns a list of the pending propositions. + + { + "status" : { ... }, + "propositions" : [ + { + "id" : "id-goes-here", + "name" : "Rename Station", + "description" : "Rename the station from 'Bri Prui 7' to 'Deep Space 1'.", + "votes_needed" : 7, + "votes_yes" : 1, + "votes_no" : 0, + "status" : "Pending", + "date_ends" : "01 31 2010 13:09:05 +0600" + }, + ... + ] + } + +=head3 session_id + +A session id. + +=head3 building_id + +The unique id of the parliament. + + + + + +=head2 cast_vote ( session_id, building_id, proposition_id, vote ) + +Casts a vote for or against a proposition. + + { + "status" : { ... }, + "proposition" : { + "id" : "id-goes-here", + "name" : "Rename Station", + "description" : "Rename the station from 'Bri Prui 7' to 'Deep Space 1'.", + "votes_needed" : 7, + "votes_yes" : 2, + "votes_no" : 0, + "status" : "Pending", + "date_ends" : "01 31 2010 13:09:05 +0600" + } + } + + +=head3 session_id + +A session id. + +=head3 building_id + +The unique id of the parliament. + +=head3 proposition_id + +The id of the propostion your casting this vote for or against. See C for a list. + +=head3 vote + +A boolean indicating which way you wish to vote. 1 for yes. 0 for no. Default is 0. + =cut \ No newline at end of file diff --git a/lib/Lacuna/DB/Result/Alliance.pm b/lib/Lacuna/DB/Result/Alliance.pm index ed539cd8..ed9395a9 100644 --- a/lib/Lacuna/DB/Result/Alliance.pm +++ b/lib/Lacuna/DB/Result/Alliance.pm @@ -23,6 +23,7 @@ __PACKAGE__->belongs_to('leader', 'Lacuna::DB::Result::Empire', 'leader_id', { o __PACKAGE__->has_many('members', 'Lacuna::DB::Result::Empire', 'alliance_id'); __PACKAGE__->has_many('invites', 'Lacuna::DB::Result::AllianceInvite', 'alliance_id'); __PACKAGE__->has_many('stations', 'Lacuna::DB::Result::Map::Body', 'alliance_id'); +__PACKAGE__->has_many('propositions', 'Lacuna::DB::Result::Propositions', 'alliance_id'); @@ -240,12 +241,28 @@ sub remove_member { before delete => sub { my $self = shift; + my $propositions = $self->propositions; + while (my $proposition = $propositions->next) { + $proposition->delete; + } + my $stations = $self->stations; + while (my $station = $stations->next) { + $station->sanitize; + } my $members = $self->members; while (my $member = $members->next) { $self->remove_member($member, 1); } }; +sub send_predefined_message { + my ($self, %options) = @_; + my $members = $self->members; + while (my $empire = $members->next) { + $empire->send_predefined_message(%options); + } +} + no Moose; __PACKAGE__->meta->make_immutable(inline_constructor => 0); diff --git a/lib/Lacuna/DB/Result/Building/Module/Parliament.pm b/lib/Lacuna/DB/Result/Building/Module/Parliament.pm index c44b6c2b..01341036 100644 --- a/lib/Lacuna/DB/Result/Building/Module/Parliament.pm +++ b/lib/Lacuna/DB/Result/Building/Module/Parliament.pm @@ -10,7 +10,10 @@ use constant image => 'parliament'; use constant name => 'Parliament'; use constant max_instances_per_planet => 1; - +sub propositions { + my ($self) = @_; + return Lacuna->db->resultset('Lacuna::DB::Result::Propositions')->search({station_id => $self->body->id}); +} no Moose; __PACKAGE__->meta->make_immutable(inline_constructor => 0); diff --git a/lib/Lacuna/DB/Result/Propositions.pm b/lib/Lacuna/DB/Result/Propositions.pm index 2628d77e..e8ed427e 100644 --- a/lib/Lacuna/DB/Result/Propositions.pm +++ b/lib/Lacuna/DB/Result/Propositions.pm @@ -5,10 +5,10 @@ use utf8; no warnings qw(uninitialized); extends 'Lacuna::DB::Result'; use Lacuna::Util qw(format_date); -use Lacuna::Constants qw(FOOD_TYPES ORE_TYPES); use DateTime; -__PACKAGE__->table('alliance'); +__PACKAGE__->load_components('DynamicSubclass'); +__PACKAGE__->table('propositions'); __PACKAGE__->add_columns( name => { data_type => 'varchar', size => 30, is_nullable => 0 }, station_id => { data_type => 'int', size => 11, is_nullable => 0 }, @@ -16,40 +16,159 @@ __PACKAGE__->add_columns( votes_yes => { data_type => 'int', is_nullable => 0, default_value => 0 }, votes_no => { data_type => 'int', is_nullable => 0, default_value => 0 }, description => { data_type => 'text', is_nullable => 1 }, - consequence => { data_type => 'text', is_nullable => 1 }, - type => { data_type => 'varchar', size => 15, is_nullable => 0 }, + type => { data_type => 'varchar', size => 30, is_nullable => 0 }, scratch => { data_type => 'mediumblob', is_nullable => 1, 'serializer_class' => 'JSON' }, + date_ends => { data_type => 'datetime', is_nullable => 0 }, + status => { data_type => 'varchar', size => 10, is_nullable => 0, default_value => 'Pending' }, ); +__PACKAGE__->typecast_map(type => { + RenameStation => 'Lacuna::DB::Result::Propositions::RenameStation', +}); + __PACKAGE__->belongs_to('station', 'Lacuna::DB::Result::Map::Body', 'station_id'); +__PACKAGE__->has_many('votes', 'Lacuna::DB::Result::Votes', 'proposition_id'); +sub sqlt_deploy_hook { + my ($self, $sqlt_table) = @_; + $sqlt_table->add_index(name => 'idx_status_date_ends', fields => ['status','date_ends']); +} -sub date_created_formatted { - my ($self) = @_; - return format_date($self->date_created); +sub cast_vote { + my ($self, $empire, $vote) = @_; + unless ($self->status eq 'Pending') { + confess [1009, 'This proposition has already '.$self->status.'.']; + } + Lacuna->db->resultset('Lacuna::DB::Result::Votes')->new({ + proposition_id => $self->id, + empire_id => $empire->id, + vote => $vote, + })->insert; + if ($vote) { + $self->votes_yes( $self->votes_yes + 1 ); + } + else { + $self->votes_no( $self->votes_no + 1 ); + } + $self->check_status; } -sub get_status { +before delete => sub { + my $self = shift; + $self->votes->delete_all; +}; + +sub check_status { my $self = shift; - my $members = $self->members; - my @members_list; - while (my $member = $members->next) { - push @members_list, { - empire_id => $member->id, - name => $member->name, - }; + if ($self->status ne 'Pending') { } + elsif ($self->votes_yes >= $self->votes_needed) { + $self->pass; + } + elsif ($self->votes_no >= $self->votes_needed) { + $self->fail; + } + elsif ($self->date_ends->epoch < time()) { + $self->pass; + } + $self->update; + return $self; +} + +sub pass { + my $self = shift; + $self->status('Passed'); + $self->station->alliance->send_message( + filename => 'parliament_vote_passed.txt', + tag => 'Correspondence', + params => [ + $self->name, + $self->name, + $self->votes_yes, + $self->votes_no, + $self->description, + $self->fail_extra_message, + ], + ); + return $self; +} + +has pass_extra_message => ( + is => 'rw', +); + +sub fail { + my $self = shift; + $self->status('Failed'); + $self->station->alliance->send_message( + filename => 'parliament_vote_failed.txt', + tag => 'Correspondence', + params => [ + $self->name, + $self->name, + $self->votes_yes, + $self->votes_no, + $self->description, + $self->fail_extra_message, + ], + ); + return $self; +} + +has fail_extra_message => ( + is => 'ro', +); + +before insert => sub { + my $self = shift; + $self->votes_needed( int($self->station->alliance->members->count / 2) ); + $self->date_ends( DateTime->now->add(hours => 72) ); +}; + +after insert => sub { + my $self = shift; + $self->send_vote; +}; + +sub get_status { + my $self = shift; return { - id => $self->id, - members => \@members_list, - leader_id => $self->leader_id, - forum_uri => $self->forum_uri, - description => $self->description, - name => $self->name, - announcements => $self->announcements, - date_created => $self->date_created_formatted, + id => $self->id, + name => $self->name, + description => $self->description, + votes_needed=> $self->votes_needed, + votes_yes => $self->votes_yes, + votes_no => $self->votes_no, + status => $self->status, + date_ends => $self->date_ends_formatted, }; } +sub send_vote { + my $self = shift; + my $station = $self->station; + my $parliament = $station->get_building_of_type('Lacuna::DB::Result::Building::Module::Parliament'); + $station->alliance->send_message( + filename => 'parliament_vote.txt', + tag => 'Correspondence', + params => [ + $self->name, + $self->name, + $self->description, + $station->id, + $parliament->id, + $self->id, + $station->id, + $parliament->id, + $self->id, + ], + ); +} + +sub date_ends_formatted { + my ($self) = @_; + return format_date($self->date_ends); +} + no Moose; __PACKAGE__->meta->make_immutable(inline_constructor => 0); diff --git a/lib/Lacuna/DB/Result/Propositions/RenameStation.pm b/lib/Lacuna/DB/Result/Propositions/RenameStation.pm new file mode 100644 index 00000000..2a3f35fd --- /dev/null +++ b/lib/Lacuna/DB/Result/Propositions/RenameStation.pm @@ -0,0 +1,26 @@ +package Lacuna::DB::Result::Propositions::RenameStation; + +use Moose; +use utf8; +no warnings qw(uninitialized); +extends 'Lacuna::DB::Result::Propositions'; + +before pass => sub { + my ($self) = @_; + my $station = $self->station; + my $name = $self->scratch->{name}; + if (Lacuna->db->resultset('Lacuna::DB::Result::Map::Body')->search({name=>$name, 'id'=>{'!='=>$station->id}})->count) { + $self->pass_extra_message('Unfortunately by the time the proposition passed, the name *'.$name.'* had already taken, effectively nullifying the vote.'); + } + else { + $self->station->name($name); + } +}; + +before fail => sub { + my ($self) = @_; +}; + + +no Moose; +__PACKAGE__->meta->make_immutable(inline_constructor => 0); diff --git a/lib/Lacuna/DB/Result/Votes.pm b/lib/Lacuna/DB/Result/Votes.pm index 04a88d77..815eecdf 100644 --- a/lib/Lacuna/DB/Result/Votes.pm +++ b/lib/Lacuna/DB/Result/Votes.pm @@ -8,7 +8,7 @@ use Lacuna::Util qw(format_date); use Lacuna::Constants qw(FOOD_TYPES ORE_TYPES); use DateTime; -__PACKAGE__->table('alliance'); +__PACKAGE__->table('votes'); __PACKAGE__->add_columns( proposition_id => { data_type => 'int', is_nullable => 0 }, empire_id => { data_type => 'int', is_nullable => 0 }, @@ -19,32 +19,5 @@ __PACKAGE__->belongs_to('proposition', 'Lacuna::DB::Result::Propositions', 'prop __PACKAGE__->belongs_to('empire', 'Lacuna::DB::Result::Empire', 'empire_id'); -sub date_created_formatted { - my ($self) = @_; - return format_date($self->date_created); -} - -sub get_status { - my $self = shift; - my $members = $self->members; - my @members_list; - while (my $member = $members->next) { - push @members_list, { - empire_id => $member->id, - name => $member->name, - }; - } - return { - id => $self->id, - members => \@members_list, - leader_id => $self->leader_id, - forum_uri => $self->forum_uri, - description => $self->description, - name => $self->name, - announcements => $self->announcements, - date_created => $self->date_created_formatted, - }; -} - no Moose; __PACKAGE__->meta->make_immutable(inline_constructor => 0); diff --git a/lib/Lacuna/RPC/Body.pm b/lib/Lacuna/RPC/Body.pm index 4e2d3dcb..c1ba89ed 100644 --- a/lib/Lacuna/RPC/Body.pm +++ b/lib/Lacuna/RPC/Body.pm @@ -40,8 +40,16 @@ sub rename { my $empire = $self->get_empire_by_session($session_id); my $body = $self->get_body($empire, $body_id); - if ($body->isa('Lacuna::DB::Result::Map::Body::Planet::SpaceStation')) { - confess [1010, 'Space stations can only be renamed through an act of Parliament.']; + if ($body->isa('Lacuna::DB::Result::Map::Body::Planet::SpaceStation')) { + my $proposition = Lacuna->db->resultset('Lacuna::DB::Result::Propositions')->new({ + type => 'RenameStation', + name => 'Rename Station', + scratch => { name => $name }, + description => 'Rename the station from "'.$body->name.'" to "'.$name.'".', + }); + $proposition->station($body); + $proposition->insert; + confess [1017, 'The rename has been delayed pending a parliamentary vote.']; } return 1 if $name eq $body->name; diff --git a/lib/Lacuna/RPC/Building/Parliament.pm b/lib/Lacuna/RPC/Building/Parliament.pm index 995f6dce..e27aa369 100644 --- a/lib/Lacuna/RPC/Building/Parliament.pm +++ b/lib/Lacuna/RPC/Building/Parliament.pm @@ -13,8 +13,46 @@ sub model_class { return 'Lacuna::DB::Result::Building::Module::Parliament'; } +sub view_propositions { + my ($self, $session_id, $building_id) = @_; + my $empire = $self->get_empire_by_session($session_id); + my $building = $self->get_building($empire, $building_id); + my @out; + my $propositions = $building->propositions->search({ status => 'Pending'}); + while (my $proposition = $propositions->next) { + $proposition->check_status; + push @out, $proposition->get_status; + } + return { + status => $self->format_status($empire, $building->body), + propositions => \@out, + }; +} + +sub cast_vote { + my ($self, $session_id, $building_id, $proposition_id, $vote) = @_; + my $empire = $self->get_empire_by_session($session_id); + my $building = $self->get_building($empire, $building_id); + my $cache = Lacuna->cache; + my $lock = 'vote_lock_'.$empire->id; + if ($cache->get($lock, $proposition_id)) { + confess [1013, 'You already have a vote in process for this proposition.']; + } + $cache->set($lock,$proposition_id,1,5); + my $guard = guard {$cache->delete($lock,$proposition_id);}; + my $proposition = Lacuna->db->resultset('Lacuna::DB::Result::Propositions')->find($proposition_id); + unless (defined $proposition) { + confess [1002, 'Proposition not found.']; + } + $proposition->cast_vote($self->empire, $vote); + return { + status => $self->format_status($empire, $building->body), + proposition => $proposition->get_status, + }; +} + -__PACKAGE__->register_rpc_method_names(qw()); +__PACKAGE__->register_rpc_method_names(qw(view_propositions)); no Moose; __PACKAGE__->meta->make_immutable; diff --git a/var/messages/parliament_vote.txt b/var/messages/parliament_vote.txt new file mode 100644 index 00000000..75d9ac36 --- /dev/null +++ b/var/messages/parliament_vote.txt @@ -0,0 +1,12 @@ +%s +~~~ +A parliamentary vote has been called on a proposition titled *%s*. Here are the particulars: + +%s + + +Do you agree to this proposition? + +{VoteYes %s %s %s} {VoteNo %s %s %s} + +This proposition will automatically pass without a majority vote 72 hours from now. \ No newline at end of file diff --git a/var/messages/parliament_vote_failed.txt b/var/messages/parliament_vote_failed.txt new file mode 100644 index 00000000..27b867d4 --- /dev/null +++ b/var/messages/parliament_vote_failed.txt @@ -0,0 +1,7 @@ +Fail: %s +~~~ +The parliamentary vote for the proposition titled *%s* has failed with %s votes for and %s votes against. As a reminder, here are the particulars: + +%s + +%s \ No newline at end of file diff --git a/var/messages/parliament_vote_passed.txt b/var/messages/parliament_vote_passed.txt new file mode 100644 index 00000000..35245157 --- /dev/null +++ b/var/messages/parliament_vote_passed.txt @@ -0,0 +1,7 @@ +Pass: %s +~~~ +The parliamentary vote for the proposition titled *%s* has passed with %s votes for and %s votes against. As a reminder, here are the particulars: + +%s + +%s \ No newline at end of file diff --git a/var/www/public/changes.txt b/var/www/public/changes.txt index 4e07e53b..119efa99 100644 --- a/var/www/public/changes.txt +++ b/var/www/public/changes.txt @@ -1,5 +1,6 @@ 3.0200 - Add: Added Amalgus Meadow and Denton Brambles glyph buildings. + - Add: Added {VoteYes 0 0 0} and {VoteNo 0 0 0} formatters to the email DSL. - Rule: Surveyors now return efficiency information in the table of building data. - API: Slight change to the Space Station Lab API to accomodate displaying descriptions and images for the plans. - Fix: P16 type planets were being misidentified as P17 planets.