diff --git a/lib/CF/Controller/Posts.pm b/lib/CF/Controller/Posts.pm index 17b4bc8e5290a8f2f43ade6ef1739d2b91dc2d95..eac8110effbc218ced88432ba93b1d9600a8a7e9 100644 --- a/lib/CF/Controller/Posts.pm +++ b/lib/CF/Controller/Posts.pm @@ -1,12 +1,18 @@ package CF::Controller::Posts; + use Mojo::Base 'Mojolicious::Controller'; +use Mojo::Pg::PubSub; +use feature 'signatures'; +no warnings qw{ experimental::signatures }; + +sub create ($c) { + $c->openapi->valid_input or return; -sub create { - my $c = shift->openapi->valid_input or return; - my $args = $c->req->json; + my $args = $c->req->json; + my $pubsub = Mojo::Pg::PubSub->new(pg => $c->pg); # Navrh postupu muze predlozit jenom clen - if ( $args->{type} == 0 && ! $c->user_roles->{'xember'} ) { + if ( $args->{type} == 0 && ! $c->user_roles->{member} ) { return $c->error(401, 'Insufficient permissions'); } @@ -16,7 +22,9 @@ sub create { content => $args->{content}, }); - ### TODO: Notify + $pubsub->json('posts')->notify( + posts => $post->view->format() + ); $c->render( status => 201, @@ -24,4 +32,171 @@ sub create { ); }; +sub get ($c) { + $c->openapi->valid_input or return; + + my $post = $c->schema->resultset('Post_view')->find($c->stash->{id}); + return $c->error(404, 'Post not found') if ! $post; + + $c->render(openapi => $c->spec_filter($post->format(), 'Post')); +} + +sub list ($c) { + $c->openapi->valid_input or return; + my $args = $c->validation->output; + + my ($cond, $attrs) = $c->search_parametrs( $args ); + + $cond->{type} = $args->{type}; + + if ( exists $args->{archived} && defined $args->{archived}) { + $cond->{is_archived} = $args->{archived}; + } + + # neschvalene navrhy postupu + if ( $args->{type} == 0 && ! $c->user_roles->{chairman} ) { + $cond->{state} = {'-in' => [1,2,3,4]} + } + + my @posts = (); + + my $count = $c->schema->resultset('Post_view')->count($cond); + + if ( $count ) { + my $posts = $c->schema->resultset('Post_view')->search($cond, $attrs); + + RECORD: + while ( my $post = $posts->next() ) { + push @posts, $c->spec_filter($post->format(), 'Post'); + } + } + + $c->render(json => { + data => \@posts, + total => $count, + }); +} + +sub update ($c) { + $c->openapi->valid_input or return; + + my $args = $c->req->json; + my $pubsub = Mojo::Pg::PubSub->new(pg => $c->pg); + my $update = {}; + + my $post = $c->schema->resultset('Post')->find($c->stash->{id}); + return $c->error(404, 'Post not found') if ! $post; + + if ( ! $c->user_roles->{chairman} ) { + if ( $post->user_id != $c->user->{id} ) { + return $c->error(403, 'Access deined'); + } + else { + delete $args->{is_archived}; + delete $args->{state}; + } + } + + foreach my $key (qw(is_archoved state content)) { + $update->{$key} = $args->{$key} if exists $args->{$key}; + } + + my $guard = $c->schema->txn_scope_guard; + + $post->add_to_history({ + user_id => $c->user->{id}, + datetime => $post->datetime, + content => $post->content, + }); + + $post->update( $update ); + + $pubsub->json('posts')->notify( + posts => $post->view->format() + ); + + $guard->commit; + + $c->render(status => 204, text => ''); +} + +sub ranking ($c) { + $c->openapi->valid_input or return; + my $args = $c->validation->output; + my $pubsub = Mojo::Pg::PubSub->new(pg => $c->pg); + + my $post = $c->schema->resultset('Post')->find($c->stash->{id}); + return $c->error(404, 'Post not found') if ! $post; + + my $user_ranking = $post->rankings({ + user_id => $c->user->{id}, + })->first; + + my $update = { + ranking_likes => $post->ranking_likes, + ranking_dislikes => $post->ranking_dislikes, + }; + + my $user_ranking_update; + + if ($user_ranking && $user_ranking->ranking == $args->{ranking} ) { + $user_ranking_update = 0; + } + else { + $user_ranking_update = $args->{ranking}; + } + + if ($user_ranking && $user_ranking->ranking == 1 ) { + $update->{ranking_likes}--; + $update->{ranking_dislikes}++ if $args->{ranking} == -1; + } + elsif ($user_ranking && $user_ranking->ranking == -1 ) { + $update->{ranking_dislikes}--; + $update->{ranking_likes}++ if $args->{ranking} == 1; + } + else { + $update->{ranking_likes}++ if $args->{ranking} == 1; + $update->{ranking_dislikes}++ if $args->{ranking} == -1; + } + + my $guard = $c->schema->txn_scope_guard; + + $post->update( $update ); + + if ( $user_ranking ) { + $user_ranking->update( { ranking => $user_ranking_update } ); + } + else { + $post->add_to_rankings({ + user_id => $c->user->{id}, + ranking => $user_ranking_update, + }); + } + + $pubsub->json('posts')->notify( + posts => $post->view->format() + ); + $guard->commit; + + $c->render(status => 204, text => ''); +} + +sub ws { + my $c = shift; + + $c->inactivity_timeout(300); + + my $pubsub = Mojo::Pg::PubSub->new(pg => $c->pg); + + $pubsub->listen(posts => sub($pubsub, $payload) { + # FILTER? + $c->send($payload); + }); + + $c->on(finish => sub ($c, $code, $reason = undef) { + $pubsub->unlisten('streams'); + $c->app->log->debug("WebSocket closed with status $code"); + }); +} + 1; diff --git a/lib/CF/Schema/Result/Post.pm b/lib/CF/Schema/Result/Post.pm index 17ba0b9ecdfcc5889575c095d760f9760ce8b440..03d004d4ccc6b333fac72bd978aedeefeda6d380 100644 --- a/lib/CF/Schema/Result/Post.pm +++ b/lib/CF/Schema/Result/Post.pm @@ -30,5 +30,23 @@ __PACKAGE__->add_columns( __PACKAGE__->set_primary_key('id'); -1; +__PACKAGE__->has_one( view => 'CF::Schema::Result::Post_view', 'id'); +__PACKAGE__->belongs_to( + user => 'Zircon::Schema::Result::User', + { + 'foreign.id' => 'self.user_id', + }, +); + +__PACKAGE__->has_many( + history => 'CF::Schema::Result::PostHistory', + { 'foreign.post_id' => 'self.id', }, +); + +__PACKAGE__->has_many( + rankings => 'CF::Schema::Result::PostRanking', + { 'foreign.post_id' => 'self.id', }, +); + +1; diff --git a/lib/CF/Schema/Result/PostHistory.pm b/lib/CF/Schema/Result/PostHistory.pm new file mode 100644 index 0000000000000000000000000000000000000000..8888e5b72a2932031a0cdd288cc62ef2074ca7d1 --- /dev/null +++ b/lib/CF/Schema/Result/PostHistory.pm @@ -0,0 +1,45 @@ +package CF::Schema::Result::PostHistory; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +our $VERSION = 1; + +__PACKAGE__->table('posts_history'); + +__PACKAGE__->add_columns( + id => { + data_type => 'integer', + is_auto_increment => 1, + is_nullable => 0, + sequence => 'uid_seq' + }, + qw( + datetime + user_id + post_id + content + ), +); + +__PACKAGE__->set_primary_key('id'); + +__PACKAGE__->has_one( view => 'CF::Schema::Result::Post_view', 'id'); + +__PACKAGE__->belongs_to( + user => 'Zircon::Schema::Result::User', + { + 'foreign.id' => 'self.user_id', + }, +); + +__PACKAGE__->belongs_to( + post => 'Zircon::Schema::Result::Post', + { + 'foreign.id' => 'self.post_id', + }, +); + +1; diff --git a/lib/CF/Schema/Result/PostRanking.pm b/lib/CF/Schema/Result/PostRanking.pm new file mode 100644 index 0000000000000000000000000000000000000000..e55ad51a1cb54e902e38ee906220cdf4cfb7532f --- /dev/null +++ b/lib/CF/Schema/Result/PostRanking.pm @@ -0,0 +1,48 @@ +package CF::Schema::Result::PostRanking; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +our $VERSION = 1; + +__PACKAGE__->table('posts_ranking'); + +__PACKAGE__->add_columns( + id => { + data_type => 'integer', + is_auto_increment => 1, + is_nullable => 0, + sequence => 'uid_seq' + }, + qw( + user_id + post_id + ranking + ), +); + +__PACKAGE__->set_primary_key('id'); + +__PACKAGE__->add_unique_constraint( + 'post_user' => [qw(user_id post_id)] +); + +__PACKAGE__->has_one( view => 'CF::Schema::Result::Post_view', 'id'); + +__PACKAGE__->belongs_to( + user => 'Zircon::Schema::Result::User', + { + 'foreign.id' => 'self.user_id', + }, +); + +__PACKAGE__->belongs_to( + post => 'Zircon::Schema::Result::Post', + { + 'foreign.id' => 'self.post_id', + }, +); + +1; diff --git a/lib/CF/Schema/Result/Post_view.pm b/lib/CF/Schema/Result/Post_view.pm index a72981125faa1b63416e9cce05dc9a04342d0595..4ddf5c77f494bd43c23b799c4ec4ded0783c5e30 100644 --- a/lib/CF/Schema/Result/Post_view.pm +++ b/lib/CF/Schema/Result/Post_view.pm @@ -11,7 +11,7 @@ __PACKAGE__->table('posts_view'); __PACKAGE__->add_columns( qw( - score + ranking_score user_name group_name ), @@ -23,18 +23,20 @@ sub format { my $self = shift; my $post = { - id => $self->id, - datetime => $self->datetime, - type => $self->type, - is_archive => $self->is_archived, + id => $self->id, + datetime => $self->datetime, + type => $self->type, + state => $self->state, + content => $self->content, + is_archived => $self->is_archived, author => { name => $self->user_name, group => $self->group_name, }, ranking => { - score => $self->score, - likes => $self->likes, - dislikes => $self->dislikes, + score => $self->ranking_score, + likes => $self->ranking_likes, + dislikes => $self->ranking_dislikes, my_vote => 0, #TODO } }; diff --git a/openapi.yaml b/openapi.yaml index 65e0749e56bd660823e44d7592cfb495a2d400a6..a54058b83a57ce8849513c3c3c329b5a33415f4f 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -24,6 +24,25 @@ components: name: Authorization in: header type: apiKey + parameters: + offset: + name: offset + in: query + description: The number of items to skip before starting to collect the result set + required: false + schema: + type: integer + default: 0 + minimum: 0 + limit: + name: limit + in: query + description: The numbers of items to return + required: false + schema: + type: integer + default: 100 + minimum: 1 schemas: ProgramScheduleEntry: type: object @@ -67,53 +86,30 @@ components: type: string group: type: string - DiscussionPost: + Post: type: object - description: Prispevek do rozpravy + description: Prispevek properties: id: type: integer readOnly: true datetime: type: string - is_archived: - type: boolean - content: - type: string - author: - $ref: '#/components/schemas/Author' - ranking: - $ref: '#/components/schemas/Ranking' - ProposalPost: - type: object - description: NavrhPostupu - properties: - id: + type: type: integer readOnly: true - datetime: + enum: [0, 1] + state: + type: integer + enum: [0, 1, 2, 3, 4] + content: type: string is_archived: type: boolean - content: - type: string author: $ref: '#/components/schemas/Author' ranking: $ref: '#/components/schemas/Ranking' -# responses: -# Unauthorized: -# description: Unauthorized -# schema: -# $ref: '#/definitions/ErrorResponse' -# Forbidden: -# description: Forbidden -# schema: -# $ref: '#/definitions/ErrorResponse' -# NotFound: -# description: The specified resource was not found -# schema: -# $ref: '#/definitions/ErrorResponse' paths: /program: @@ -166,23 +162,133 @@ paths: id: type: integer description: Post id - get: x-mojo-to: posts#list security: - - Bearer: [] + - Bearer: ['optional', 'member', 'regp'] tags: - posts summary: "Zpravy" operationId: getPosts + parameters: + - $ref: '#/components/parameters/offset' + - $ref: '#/components/parameters/limit' + - name: type + in: query + description: "Typ zpravy" + required: true + schema: + type: integer + enum: [0, 1] + - name: archived + in: query + description: "Zobrazovat archivovane" + required: false + schema: + type: boolean + - name: sort + description: "Razeni" + in: query + style: form + schema: + type: array + uniqueItems: true + items: + type: string + enum: [ type, datetime, -type, -datetime,] + default: [ -datetime ] responses: 200: description: Posts content: application/json: schema: - type: array - items: - oneOf: - - $ref: '#/components/schemas/DiscussionPost' - - $ref: '#/components/schemas/ProposalPost' + type: object + properties: + count: + type: integer + description: Celkovy pocet + data: + type: array + items: + $ref: '#/components/schemas/Post' + /posts/{id}: + get: + x-mojo-to: posts#get + tags: + - posts + summary: "Detail zpravy" + operationId: getPost + responses: + 200: + description: Post + content: + application/json: + schema: + $ref: '#/components/schemas/Post' + put: + x-mojo-to: posts#update + security: + - Bearer: ['member', 'regp'] + tags: + - posts + summary: "Uprava zpravu" + operationId: updatePost + requestBody: + content: + application/json: + schema: + type: object + properties: + state: + type: integer + enum: [0, 1, 2, 3, 4] + content: + type: string + isr_archived: + type: boolean + responses: + 204: + description: Post updated + + /posts/{id}/like: + patch: + security: + - Bearer: ['member', 'regp'] + x-mojo-to: posts#ranking + tags: + - posts + summary: "Like" + operationId: likePost + parameters: + - name: ranking + in: query + required: false + schema: + type: integer + enum: [1] + default: 1 + responses: + 204: + description: Post liked + + /posts/{id}/dislike: + patch: + security: + - Bearer: ['member', 'regp'] + x-mojo-to: posts#ranking + tags: + - posts + summary: "Like" + operationId: dislikePost + parameters: + - name: ranking + in: query + required: false + schema: + type: integer + enum: [-1] + default: -1 + responses: + 204: + description: Post disliked diff --git a/sql/2/up.sql b/sql/2/up.sql index 5f98dd1bb32553fed86c52b3308a38cde766c855..462490230544280bf52d362905cd49f14f6e168e 100644 --- a/sql/2/up.sql +++ b/sql/2/up.sql @@ -26,6 +26,7 @@ create table "announcements" ( create view "posts_view" as select "posts".*, + "posts"."ranking_likes" - "posts"."ranking_dislikes" as "ranking_score", "users"."name" as "user_name", "users"."main_group_name" as "group_name" from posts diff --git a/sql/3/up.sql b/sql/3/up.sql new file mode 100644 index 0000000000000000000000000000000000000000..58d40fa56f1596bae2d34b4e5add6f72178e2d88 --- /dev/null +++ b/sql/3/up.sql @@ -0,0 +1,17 @@ +create table "posts_history" ( + "id" integer not null default nextval('uid_seq'), + "datetime" timestamp(0) not null default now(), + "post_id" integer not null, + "user_id" integer not null, + "content" text, + primary key("id") +); + +create table "posts_ranking" ( + "id" integer not null default nextval('uid_seq'), + "post_id" integer not null, + "user_id" integer not null, + "ranking" integer, + primary key("id"), + unique("post_id", "user_id") +);