@import "newslash/motd.less";
@import "newslash/search.less";
@import "newslash/poll.less";
-@import "newslash/progress_bar.less";
\ No newline at end of file
+@import "newslash/progress_bar.less";
+
+@import "newslash/admin/ad_codes_manager.less";
\ No newline at end of file
--- /dev/null
+/* styles for ad-codes manager */
+
+#forms-wrapper {
+ display: flex;
+ #ad-codes-selector {
+ flex: 0 0 auto;
+ margin: 0 @component-vertical-margin;
+ padding: 8px;
+ width: 300px;
+ select { width: 100%; }
+ }
+ #ad-codes-editor {
+ flex: 1 0 auto;
+ margin: 0 @component-vertical-margin;
+ padding: 8px;
+ textarea { width: 100%; }
+ }
+}
+
package Newslash::Model;
use Newslash::Model::Loader;
+load(ad_codes => "Newslash::Model::ADCodes");
load(boxes => "Newslash::Model::Boxes");
load(custom_boxes => "Newslash::Model::CustomBoxes");
load(comments => "Newslash::Model::Comments");
--- /dev/null
+package Newslash::Model::ADCodes;
+use Newslash::Model::Base -base;
+
+sub on_start_up {
+ my $self = shift;
+
+ # create tables
+ return 1 if $self->check_readonly;
+
+ my $sqls = {
+ ns_ad_codes => <<"EOSQL",
+CREATE TABLE IF NOT EXISTS ns_ad_codes (
+ ad_id mediumint(8) unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ name varchar(191) UNIQUE NOT NULL,
+ description varchar(255) DEFAULT "",
+ status enum('enabled', 'disabled') DEFAULT 'enabled',
+ content mediumtext,
+ last_update timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+) DEFAULT CHARSET=utf8mb4
+EOSQL
+ };
+
+ my $dbh = $self->connect_db;
+ for my $table_name (keys %$sqls) {
+ if (!$self->table_exists($table_name)) {
+ my $rs = $dbh->do($sqls->{$table_name}, undef);
+ if ($rs) {
+ $self->warn("create table: $table_name");
+ }
+ else {
+ $self->warn("cannot create table: $table_name");
+ }
+ }
+ }
+ $self->disconnect_db;
+
+ return 1;
+}
+
+sub select {
+ my $self = shift;
+ my $params = {@_};
+
+ my $unique_keys = { id => "ns_ad_codes.ad_id",
+ ad_id => "ns_ad_codes.ad_id",
+ name => "ns_ad_codes.name",
+ };
+ my $keys = { last_update => "ns_ad_codes.last_update",
+ status => "ns_ad_codes.status",
+ };
+ my $timestamp = "ns_ad_codes.last_update";
+
+ my ($where_clause, $where_values, $unique) = $self->build_where_clause(unique_keys => $unique_keys,
+ keys => $keys,
+ timestamp => $timestamp,
+ params => $params);
+ my ($limit_clause, $limit_values) = $self->build_limit_clause(params => $params);
+ my ($orderby_clause, $orderby_values) = $self->build_order_by_clause(keys => $keys,
+ params => $params);
+
+ my @attrs;
+ push @attrs, @$where_values, @$limit_values, @$orderby_values;
+
+ my $dbh = $self->connect_db;
+ my $sql = <<"EOSQL";
+SELECT * FROM ns_ad_codes
+ $where_clause
+ $orderby_clause
+ $limit_clause
+EOSQL
+
+ #warn($sql);
+ #warn(Dumper(@attrs));
+
+ my $sth = $dbh->prepare($sql);
+ $sth->execute(@attrs);
+ my $feeds = $sth->fetchall_arrayref({});
+
+ if (!$feeds) {
+ $self->disconnect_db();
+ return;
+ }
+ if (@$feeds == 0) {
+ $self->disconnect_db();
+ return $unique ? undef : [];
+ }
+
+ return $feeds->[0] if $unique;
+ return $feeds;
+}
+
+sub create {
+ my $self = shift;
+ my $params = {@_};
+ my $name = $params->{name};
+ return if !$name;
+
+ my $dbh = $self->connect_db;
+ my $sql = <<"EOSQL";
+INSERT INTO ns_ad_codes
+ (name, description, status, content, last_update)
+ VALUES
+ (?, ?, ?, ?, NOW() )
+EOSQL
+
+ my $rs = $dbh->do($sql, undef,
+ $name,
+ $params->{description},
+ $params->{status} || 'enabled',
+ $params->{content}
+ );
+ return if !$rs;
+ my $id = $dbh->last_insert_id(undef, undef, undef, undef);
+ return $id;
+}
+
+sub update {
+ my $self = shift;
+ my $params = {@_};
+ my $ad_id = $params->{ad_id} || $params->{id};
+ return if !$ad_id;
+
+ my @cols;
+ my @attrs;
+ for my $k (qw|name description status content|) {
+ if (defined $params->{$k}) {
+ push @cols, "$k = ?";
+ push @attrs, $params->{$k};
+ }
+ }
+ my $columns = join(", ", @cols);
+ my $sql = <<"EOSQL";
+UPDATE ns_ad_codes
+ SET $columns
+ WHERE ad_id = ?
+EOSQL
+
+ push @attrs, $ad_id;
+ my $dbh = $self->connect_db;
+ my $rs = $dbh->do($sql, undef, @attrs);
+ return if !$rs;
+ return $ad_id;
+}
+
+sub delete {
+ my $self = shift;
+ my $params = {@_};
+ my $ad_id = $params->{id} || $params->{ad_id};
+ return if !$ad_id;
+
+ my $sql = <<"EOSQL";
+DELETE FROM ns_ad_codes
+ WHERE ad_id = ?
+EOSQL
+
+ my $dbh = $self->connect_db;
+ my $rs = $dbh->do($sql, undef, $ad_id);
+ return $rs;
+}
+
+sub export_all {
+ my $self = shift;
+ my $target = shift;
+
+ if (ref($target) eq "SCALAR") {
+ $$target = $self->export_json(table => "ns_ad_codes",
+ sort_key => "name",
+ exclude => [qw(ad_id
+ last_update
+ )],
+ );
+ return 1;
+ }
+ my $output = $self->export_json(file => $target,
+ table => "ns_ad_codes",
+ sort_key => "name",
+ exclude => [qw(ad_id
+ last_update
+ )]
+ );
+ return if !$output;
+ return 1;
+}
+
+sub import_all {
+ my $self = shift;
+ my $target = shift;
+
+ my $output;
+
+ if (ref($target) eq "SCALAR") {
+ my $rs = $self->import_json(table => "ns_ad_codes",
+ json => $$target,
+ unique_key => "name",
+ exclude => [qw(ad_id
+ last_update
+ )]
+ );
+ return $rs;
+ }
+ my $rs = $self->import_json(table => "ns_ad_codes",
+ file => $target,
+ unique_key => "name",
+ exclude => [qw(ad_id
+ last_update
+ )],
+ );
+ return $rs;
+}
+
+1;
$admin->get('/sidebar')->to('admin-sidebar#index');
$admin->get('/feed')->to('admin-feed#index');
$admin->get('/blocking')->to('admin-blocking#index');
+ $admin->get('/ad')->to('admin-ads#index');
$admin->get('/repository')->to('admin-repository#index');
--- /dev/null
+/* sidebar_editor.js */
+const editor = {};
+var vm;
+
+editor.run = function run (params) {
+ const data = {
+ message: '',
+ boxItems: [],
+ };
+ const computed = {};
+ const methods = {
+ editItem: function editItem(item) {
+ item.editing = true;
+ item.edited = true;
+ },
+ deleteItem: function deleteItem() {
+ const target = [];
+ for (var i = 0; i < this.boxItems.length; i++) {
+ if (this.boxItems[i].selected) {
+ target.push({id: this.boxItems[i].id, index: i});
+ }
+ }
+ if (target.length == 0) {
+ return;
+ }
+ const msg = "delete " + target.length + " item(s)?";
+ const result = window.confirm(msg);
+ if (result) {
+ // delete items
+ target.reverse();
+ target.forEach(item => {
+ newslash.admin.deleteBox(item.id).then(
+ (response) => { // success
+ if (response.error) {
+ this.message = "error: " + response.message;
+ return;
+ }
+ this.boxItems.splice(item.index, 1);
+ },
+ (response) => { // fail
+ this.message = "error";
+ }
+ );
+ });
+ }
+ },
+ newItem: function newItem() {
+ const item = {
+ id: 0,
+ name: 'new item',
+ comment: '',
+ model: '',
+ query_params: '',
+ limit: '',
+ template: '',
+ selected: false,
+ editing: true,
+ edited: true,
+ };
+ const it = this.boxItems.push(item);
+ },
+ };
+
+ function initItems (items) {
+ items.forEach(item => {
+ Vue.set(item, 'selected', false);
+ Vue.set(item, 'editing', false);
+ Vue.set(item, 'edited', false);
+ });
+ };
+
+ vm = new Vue({el: params.el,
+ data: data,
+ computed: computed,
+ methods: methods,
+ created: function () {
+ newslash.admin.getBoxes().then(
+ (resp) => { // success
+ this.boxItems = resp.items || [];
+ this.message = resp.message || '';
+ initItems(this.boxItems);
+ },
+ (resp) => { // fail
+ this.boxItems = [];
+ this.message = resp.message || "failed to get items";
+ }
+ );
+ },
+ });
+};
--- /dev/null
+# -*-Perl-*-
+# Newslash::Model::ADCodes related tests
+use Mojo::Base -strict;
+
+use Test::More;
+use Test::Mojo;
+use Data::Dumper;
+use DateTime::Format::MySQL;
+
+my $t = Test::Mojo->new('Newslash::Web');
+
+my $ad_codes = $t->app->model('ad_codes');
+ok($ad_codes, "get AdCodes instance");
+
+subtest 'create/select/update/delete ad_codes and items' => sub {
+ plan skip_all => "mode is not 'test'" if ($t->app->mode ne 'test');
+
+ my $test_name = "test_feed";
+ my $test_content = "テストのコード本体";
+
+ my $rs = $ad_codes->select(name => $test_name);
+ $ad_codes->delete(ad_id => $rs->{ad_id}) if $rs;
+
+ my $ad_id = $ad_codes->create(name => $test_name,
+ content => $test_content);
+ ok($ad_id, "create ad_code");
+
+ my $ad_code = $ad_codes->select(ad_id => $ad_id);
+ ok($ad_code, "select ad_code by ad_id");
+ is($ad_code->{name}, $test_name, "retrive valid name");
+ is($ad_code->{status}, "enabled", "retrive valid status");
+ is($ad_code->{content}, $test_content, "retrive valid content");
+
+ $ad_code = $ad_codes->select(name => $test_name);
+ ok($ad_code, "select ad_code by name");
+ is($ad_code->{name}, $test_name, "retrive valid ad_code");
+
+ my $test_description = "説明2";
+ ok($ad_codes->update(ad_id => $ad_id,
+ description => $test_description),
+ "update ad_code");
+
+ ok($ad_codes->delete(id => $ad_id), "delete item");
+};
+
+
+# cleanup
+
+done_testing();
<!-- CSS -->
<li class="dropdown">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">CSS<span class="caret"></span></a>
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">コンテンツ<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="/admin/css">CSS更新</a></li>
+ <li><a href="/admin/ad">広告コード</a></li>
</ul>
</li>
--- /dev/null
+[% WRAPPER common/layout %]
+
+<div class="app-frame" id="ad-code-manager">
+ <h3>AD Code Manager</h3>
+ <div v-text="message">
+ </div>
+ <form>
+ <div id="forms-wrapper">
+ <div id="ad-codes-selector">
+ <select size="30"></select>
+ </div>
+
+ <div id="ad-codes-editor">
+ <div>
+ <label>AD_CODE_ID<input /></label>
+ </div>
+ <div>
+ <label>説明<input /></label>
+ </div>
+ <div>
+ <label>広告コード:
+ <textarea></textarea>
+ </label>
+ </div>
+ <div>
+ <button class="btn btn-default" type="button">保存</button>
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
+
+<script src="/js/ad-codes-editor.js" ></script>
+<script>
+ /* editor.run({ el: '#ad-code-manager' }); */
+</script>
+
+[% END %]