Options +ExecCGI
AddHandler cgi-script pl
DirectoryIndex index.pl
package Conf;
use warnings;
use strict;
BEGIN
{
use Exporter;
our (@ISA, @EXPORT);
@ISA = qw(Exporter);
@EXPORT = qw(
$DB_Host $DB_Port $DB_Name $DB_User $DB_Pass
);
}
our $DB_Host = "host";
our $DB_Port = 3306;
our $DB_Name = "our_db";
our $DB_User = "our_table";
our $DB_Pass = "our_password";
1;
create table users (
id smallint not null primary key auto_increment,
name varchar(32) not null,
pass varchar(32) not null);
create table categories (
id int not null primary key auto_increment,
name varchar(128) not null) charset cp1251;
create table articles (
id int not null primary key auto_increment,
category_id int not null,
title varchar(255) not null,
content text not null,
author varchar(128) not null comment 'Author of article',
added_at timestamp not null,
added_by smallint not null comment 'Admin user ID') charset cp1251;
create table news (
id int not null primary key auto_increment,
added_at timestamp not null,
title varchar(255) not null,
content text not null,
is_put_on_main bool not null default 0 comment 'Show on main page?',
added_by smallint not null) charset cp1251;
# file User.pm
package DB::User;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/PK::Auto Core/);
__PACKAGE__->table('users');
__PACKAGE__->add_columns(qw/id name pass/);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->has_many('articles' => 'DB::Article',
{ 'foreign.added_by' => 'self.id' });
__PACKAGE__->has_many('news' => 'DB::News',
{ 'foreign.added_by' => 'self.id' });
1;
# file Category.pm
package DB::Category;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/PK::Auto Core/);
__PACKAGE__->table('categories');
__PACKAGE__->add_columns(qw/id name/);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->has_many('articles' => 'DB::Article',
{ 'foreign.category_id' => 'self.id' });
1;
# file Article.pm
package DB::Article;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/InflateColumn::DateTime PK::Auto Core/);
__PACKAGE__->table('articles');
__PACKAGE__->add_columns(qw/id category_id title content added_by author/);
__PACKAGE__->add_columns('added_at' => { data_type => 'timestamp' });
__PACKAGE__->set_primary_key('id');
__PACKAGE__->belongs_to('category' => 'DB::Category',
{ 'foreign.id' => 'self.category_id' });
__PACKAGE__->belongs_to('user' => 'DB::User',
{ 'foreign.id' => 'self.added_by' });
1;
# file News.pm
package DB::News;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/InflateColumn::DateTime PK::Auto Core/);
__PACKAGE__->table('news');
__PACKAGE__->add_columns(qw/id title content is_put_on_main added_by/);
__PACKAGE__->add_columns('added_at' => { data_type => 'timestamp' });
__PACKAGE__->set_primary_key('id');
__PACKAGE__->belongs_to('user' => 'DB::User',
{ 'foreign.id' => 'self.added_by' });
1;
package SDB;
use base qw/DBIx::Class::Schema/;
use Conf;
__PACKAGE__->load_classes();
sub GetSchema()
{
my $dsn = "dbi:mysql:$DB_Name:$DB_Host";
my $sch = __PACKAGE__->connect($dsn, $DB_User, $DB_Pass);
return $sch;
}
1;
my $dsn = "dbi:mysql:$DB_Name:$DB_Host";
my $sch = DB->connect($dsn, $DB_User, $DB_Pass);
package DB;
use base qw/DBIx::Class::Schema::Loader/;
__PACKAGE__->loader_options(
inflect_singular => 1,
components => qw/InflateColumn::DateTime/
);
1;
#
use DB;
my $sch = DB->connect( $dsn, $user, $password, $attrs);
use DB;
my $sch = DB->GetShema();
# id
my $user = $sch->resultset('User')->find({ id => $id });
#
my $new_id = $sch->resultset('Category')->populate(
[
[qw/title content is_put_on_main added_by/],
[$ntitle, $ncontent, 0, $user_id]
]);
#
$sch->resultset('Article')->find({ id => $aid })->delete;
Portal
[% PROCESS $content %]
News and articles
#!/usr/bin/perl -w
use strict;
use CGI;
use Template;
use Conf;
use DB;
# CGI
my $q = CGI->new;
my %p = $q->Vars;
# ...
my $tmpl = Template->new(
{
INCLUDE_PATH => 'tmpl/site',
INTERPOLATE => 1,
EVAL_PERL => 1
}) || die "$Template::ERROR\n";
# ...
my $sch = DB->GetShema();
#
my $tmpl_vars = {};
$tmpl_vars->{content} = 'start_page';
print $q->header(-type => 'text/html', -charset => 'windows-1251');
$tmpl->process('site', $tmpl_vars) || die $tmpl->error(), "\n";
my $articles = [$sch->resultset('Article')->search(undef,
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
my $news = [$sch->resultset('News')->search(
{
is_put_on_main => 1
},
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
$tmpl_vars->{articles} = $articles;
$tmpl_vars->{news} = $news;
[% FOREACH n = news %]
[% n.added_at.dmy('.') %] [% n.title %]
[% n.content FILTER html %]
[% END %]
[% FOREACH a = articles %]
[% a.added_at.dmy('.') %] [% a.title %]
: [% a.category.name %]
[% a.content FILTER html %]
[% END %]
[% FOREACH n = news %]
[% n.added_at.dmy('.') %] [% n.title %]
[% n.content FILTER html %]
[% END %]
[% FOREACH a = articles %]
[% a.added_at.dmy('.') %] [% a.title %]
: [% a.category.name %]
[% a.content FILTER html %]
[% END %]
[% FOREACH n = news %]
[% n.added_at.dmy('.') %] [% n.title %]
[% n.content FILTER html %]
[% END %]
[% FOREACH a = articles %]
[% a.added_at.dmy('.') %] [% a.title %]
: [% a.category.name %]
[% a.content FILTER html %]
[% END %]
[% FOREACH n = news %]
[% n.added_at.dmy('.') %] [% n.title %]
[% n.content FILTER html %]
[% END %]
[% FOREACH a = articles %]
[% a.added_at.dmy('.') %] [% a.title %]
: [% a.category.name %]
[% a.content FILTER html %]
[% END %]
[% text = node.content;
IF text.length > 512;
text = text.substr(0, 512);
END %]
[% note.added_at.dmy('.') %] [% note.title %]
[% IF note.category %]
: [% note.category.name %]
[% END %]
[% text FILTER html %]
[% FOREACH n = news %]
[% PROCESS short_note note = n %]
[% END %]
[% FOREACH a = articles %]
[% PROCESS short_note note = a %]
[% END %]
[% FOREACH n = news %]
[% PROCESS short_note note = n %]
[% END %]
[% FOREACH a = articles %]
[% PROCESS short_note note = a %]
[% END %]
[% FOREACH n = news %]
[% PROCESS short_note note = n %]
[% END %]
[% FOREACH a = articles %]
[% PROCESS short_note note = a %]
[% END %]
[% FOREACH n = news %]
[% PROCESS short_note note = n %]
[% END %]
[% FOREACH a = articles %]
[% PROCESS short_note note = a %]
[% END %]
my $act = $p{'a'} || 'start';
if ($act eq 'start')
{
}
elsif ($act eq 'article')
{
}
elsif ($act eq 'news')
{
}
# ....
else
{
}
, . id , . :
$p{'id'} =~ s/\D//g if ($p{'id'});
, - , , , -. .
.
if ($act eq 'start')
{
$tmpl_vars->{content} = 'start_page';
my $articles = [$sch->resultset('Article')->search(undef,
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
my $news = [$sch->resultset('News')->search(
{
is_put_on_main => 1
},
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
$tmpl_vars->{articles} = $articles;
$tmpl_vars->{news} = $news;
}
elsif ($act eq 'article')
{
$tmpl_vars->{content} = 'full_article';
$tmpl_vars->{article} = $sch->resultset('Article')->find({ id => $p{'id'} });
}
elsif ($act eq 'category')
{
$tmpl_vars->{content} = 'category';
$tmpl_vars->{category} = $sch->resultset('Category')->find({ id => $p{'id'} });
}
elsif ($act eq 'news')
{
$tmpl_vars->{content} = 'full_article';
$tmpl_vars->{article} = $sch->resultset('News')->find({ id => $p{'id'} });
}
else
{
# .
}
, , . , Perl , , HTML-CheckArgs HTML-QuickCheck . , HTML-Widget HTML-Tag . . , . .
: , . ( , error_action , ), :
print $q->header(-location => '?a=start');
exit;
, . , , ( ):
my %action = (
'start' => 'Main page',
'news' => 'News page',
'article' => 'Full article',
# ....
);
my $act = ( $p{'act'} && defined( $actions{$p{'act'}} )) ? $p{'act'} : 'start';
, — , 'start'. - defined(...) .
. , . , tmpl/admin .
: Digest::SHA1 CGI::Session . , — .
, . .
:
[%# login %]
[% IF err %]
Wrong login
[% END %]
/>
Login: />
Password: />
/>
, :
use CGI::Session;
use Digest::SHA1 qw(sha1_hex);
# ... CGI
my $s = CGI::Session->load(undef, undef, { Directory => 'ssss' } );
# ...
if ($s->empty && $act !~ /login(_form)?|logout/)
{
print $q->header(-location => '?a=login_form');
exit;
}
else
{
my $user = $sch->resultset('User')->find({ id => $s->param('uid') });
$tmpl_vars->{user} = $user;
}
if ($act eq 'login_form')
{
$tmpl_vars->{content} = 'login_form';
}
elsif ($act eq 'login')
{
unless (my $u = &login($p{'login'}, $p{'pass'}))
{
$tmpl_vars->{content} = 'login';
$tmpl_vars->{err} = 1;
}
else
{
$s = $s->new;
$s->param('uid', $u->id);
print $s->header(-location => '?a=start');
exit;
}
}
elsif ($act eq 'logout')
{
$s->delete;
print $q->header(-location => '?a=login');
exit;
}
#
sub login
{
my ($u, $p) = @_;
my $pp = sha1_hex($p);
my $res = $sch->resultset('User')->search({
name => $u,
pass => $pp
});
my $user = $res->next;
return $user;
}
, , .
CGI::Session , . — expired. ssss .
Digest::SHA1 - MD5.
. -, CRUD- (CReate, Update, Delete). , , DBIx::Class::WebForm . CRUD CPAN .
-, . FCKeditor , . .
-, . , DBIx::Class::Validation , , , CGI::FormBuilder , CGI::QuickForm .. "Form", "Validate" "Widget" .
"" . , , , . , , . , SQL-.
, - . .
-NOT_FOR_HOLYWARS-
, . id , . :
$p{'id'} =~ s/\D//g if ($p{'id'});
, - , , , -. .
.
if ($act eq 'start')
{
$tmpl_vars->{content} = 'start_page';
my $articles = [$sch->resultset('Article')->search(undef,
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
my $news = [$sch->resultset('News')->search(
{
is_put_on_main => 1
},
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
$tmpl_vars->{articles} = $articles;
$tmpl_vars->{news} = $news;
}
elsif ($act eq 'article')
{
$tmpl_vars->{content} = 'full_article';
$tmpl_vars->{article} = $sch->resultset('Article')->find({ id => $p{'id'} });
}
elsif ($act eq 'category')
{
$tmpl_vars->{content} = 'category';
$tmpl_vars->{category} = $sch->resultset('Category')->find({ id => $p{'id'} });
}
elsif ($act eq 'news')
{
$tmpl_vars->{content} = 'full_article';
$tmpl_vars->{article} = $sch->resultset('News')->find({ id => $p{'id'} });
}
else
{
# .
}
, , . , Perl , , HTML-CheckArgs HTML-QuickCheck . , HTML-Widget HTML-Tag . . , . .
: , . ( , error_action , ), :
print $q->header(-location => '?a=start');
exit;
, . , , ( ):
my %action = (
'start' => 'Main page',
'news' => 'News page',
'article' => 'Full article',
# ....
);
my $act = ( $p{'act'} && defined( $actions{$p{'act'}} )) ? $p{'act'} : 'start';
, — , 'start'. - defined(...) .
. , . , tmpl/admin .
: Digest::SHA1 CGI::Session . , — .
, . .
:
[%# login %]
[% IF err %]
Wrong login
[% END %]
/>
Login: />
Password: />
/>
, :
use CGI::Session;
use Digest::SHA1 qw(sha1_hex);
# ... CGI
my $s = CGI::Session->load(undef, undef, { Directory => 'ssss' } );
# ...
if ($s->empty && $act !~ /login(_form)?|logout/)
{
print $q->header(-location => '?a=login_form');
exit;
}
else
{
my $user = $sch->resultset('User')->find({ id => $s->param('uid') });
$tmpl_vars->{user} = $user;
}
if ($act eq 'login_form')
{
$tmpl_vars->{content} = 'login_form';
}
elsif ($act eq 'login')
{
unless (my $u = &login($p{'login'}, $p{'pass'}))
{
$tmpl_vars->{content} = 'login';
$tmpl_vars->{err} = 1;
}
else
{
$s = $s->new;
$s->param('uid', $u->id);
print $s->header(-location => '?a=start');
exit;
}
}
elsif ($act eq 'logout')
{
$s->delete;
print $q->header(-location => '?a=login');
exit;
}
#
sub login
{
my ($u, $p) = @_;
my $pp = sha1_hex($p);
my $res = $sch->resultset('User')->search({
name => $u,
pass => $pp
});
my $user = $res->next;
return $user;
}
, , .
CGI::Session , . — expired. ssss .
Digest::SHA1 - MD5.
. -, CRUD- (CReate, Update, Delete). , , DBIx::Class::WebForm . CRUD CPAN .
-, . FCKeditor , . .
-, . , DBIx::Class::Validation , , , CGI::FormBuilder , CGI::QuickForm .. "Form", "Validate" "Widget" .
"" . , , , . , , . , SQL-.
, - . .
-NOT_FOR_HOLYWARS-
, . id , . :
$p{'id'} =~ s/\D//g if ($p{'id'});
, - , , , -. .
.
if ($act eq 'start')
{
$tmpl_vars->{content} = 'start_page';
my $articles = [$sch->resultset('Article')->search(undef,
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
my $news = [$sch->resultset('News')->search(
{
is_put_on_main => 1
},
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
$tmpl_vars->{articles} = $articles;
$tmpl_vars->{news} = $news;
}
elsif ($act eq 'article')
{
$tmpl_vars->{content} = 'full_article';
$tmpl_vars->{article} = $sch->resultset('Article')->find({ id => $p{'id'} });
}
elsif ($act eq 'category')
{
$tmpl_vars->{content} = 'category';
$tmpl_vars->{category} = $sch->resultset('Category')->find({ id => $p{'id'} });
}
elsif ($act eq 'news')
{
$tmpl_vars->{content} = 'full_article';
$tmpl_vars->{article} = $sch->resultset('News')->find({ id => $p{'id'} });
}
else
{
# .
}
, , . , Perl , , HTML-CheckArgs HTML-QuickCheck . , HTML-Widget HTML-Tag . . , . .
: , . ( , error_action , ), :
print $q->header(-location => '?a=start');
exit;
, . , , ( ):
my %action = (
'start' => 'Main page',
'news' => 'News page',
'article' => 'Full article',
# ....
);
my $act = ( $p{'act'} && defined( $actions{$p{'act'}} )) ? $p{'act'} : 'start';
, — , 'start'. - defined(...) .
. , . , tmpl/admin .
: Digest::SHA1 CGI::Session . , — .
, . .
:
[%# login %]
[% IF err %]
Wrong login
[% END %]
/>
Login: />
Password: />
/>
, :
use CGI::Session;
use Digest::SHA1 qw(sha1_hex);
# ... CGI
my $s = CGI::Session->load(undef, undef, { Directory => 'ssss' } );
# ...
if ($s->empty && $act !~ /login(_form)?|logout/)
{
print $q->header(-location => '?a=login_form');
exit;
}
else
{
my $user = $sch->resultset('User')->find({ id => $s->param('uid') });
$tmpl_vars->{user} = $user;
}
if ($act eq 'login_form')
{
$tmpl_vars->{content} = 'login_form';
}
elsif ($act eq 'login')
{
unless (my $u = &login($p{'login'}, $p{'pass'}))
{
$tmpl_vars->{content} = 'login';
$tmpl_vars->{err} = 1;
}
else
{
$s = $s->new;
$s->param('uid', $u->id);
print $s->header(-location => '?a=start');
exit;
}
}
elsif ($act eq 'logout')
{
$s->delete;
print $q->header(-location => '?a=login');
exit;
}
#
sub login
{
my ($u, $p) = @_;
my $pp = sha1_hex($p);
my $res = $sch->resultset('User')->search({
name => $u,
pass => $pp
});
my $user = $res->next;
return $user;
}
, , .
CGI::Session , . — expired. ssss .
Digest::SHA1 - MD5.
. -, CRUD- (CReate, Update, Delete). , , DBIx::Class::WebForm . CRUD CPAN .
-, . FCKeditor , . .
-, . , DBIx::Class::Validation , , , CGI::FormBuilder , CGI::QuickForm .. "Form", "Validate" "Widget" .
"" . , , , . , , . , SQL-.
, - . .
-NOT_FOR_HOLYWARS-
, . id , . :
$p{'id'} =~ s/\D//g if ($p{'id'});
, - , , , -. .
.
if ($act eq 'start')
{
$tmpl_vars->{content} = 'start_page';
my $articles = [$sch->resultset('Article')->search(undef,
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
my $news = [$sch->resultset('News')->search(
{
is_put_on_main => 1
},
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
$tmpl_vars->{articles} = $articles;
$tmpl_vars->{news} = $news;
}
elsif ($act eq 'article')
{
$tmpl_vars->{content} = 'full_article';
$tmpl_vars->{article} = $sch->resultset('Article')->find({ id => $p{'id'} });
}
elsif ($act eq 'category')
{
$tmpl_vars->{content} = 'category';
$tmpl_vars->{category} = $sch->resultset('Category')->find({ id => $p{'id'} });
}
elsif ($act eq 'news')
{
$tmpl_vars->{content} = 'full_article';
$tmpl_vars->{article} = $sch->resultset('News')->find({ id => $p{'id'} });
}
else
{
# .
}
, , . , Perl , , HTML-CheckArgs HTML-QuickCheck . , HTML-Widget HTML-Tag . . , . .
: , . ( , error_action , ), :
print $q->header(-location => '?a=start');
exit;
, . , , ( ):
my %action = (
'start' => 'Main page',
'news' => 'News page',
'article' => 'Full article',
# ....
);
my $act = ( $p{'act'} && defined( $actions{$p{'act'}} )) ? $p{'act'} : 'start';
, — , 'start'. - defined(...) .
. , . , tmpl/admin .
: Digest::SHA1 CGI::Session . , — .
, . .
:
[%# login %]
[% IF err %]
Wrong login
[% END %]
/>
Login: />
Password: />
/>
, :
use CGI::Session;
use Digest::SHA1 qw(sha1_hex);
# ... CGI
my $s = CGI::Session->load(undef, undef, { Directory => 'ssss' } );
# ...
if ($s->empty && $act !~ /login(_form)?|logout/)
{
print $q->header(-location => '?a=login_form');
exit;
}
else
{
my $user = $sch->resultset('User')->find({ id => $s->param('uid') });
$tmpl_vars->{user} = $user;
}
if ($act eq 'login_form')
{
$tmpl_vars->{content} = 'login_form';
}
elsif ($act eq 'login')
{
unless (my $u = &login($p{'login'}, $p{'pass'}))
{
$tmpl_vars->{content} = 'login';
$tmpl_vars->{err} = 1;
}
else
{
$s = $s->new;
$s->param('uid', $u->id);
print $s->header(-location => '?a=start');
exit;
}
}
elsif ($act eq 'logout')
{
$s->delete;
print $q->header(-location => '?a=login');
exit;
}
#
sub login
{
my ($u, $p) = @_;
my $pp = sha1_hex($p);
my $res = $sch->resultset('User')->search({
name => $u,
pass => $pp
});
my $user = $res->next;
return $user;
}
, , .
CGI::Session , . — expired. ssss .
Digest::SHA1 - MD5.
. -, CRUD- (CReate, Update, Delete). , , DBIx::Class::WebForm . CRUD CPAN .
-, . FCKeditor , . .
-, . , DBIx::Class::Validation , , , CGI::FormBuilder , CGI::QuickForm .. "Form", "Validate" "Widget" .
"" . , , , . , , . , SQL-.
, - . .
-NOT_FOR_HOLYWARS-
, . id , . :
$p{'id'} =~ s/\D//g if ($p{'id'});
, - , , , -. .
.
if ($act eq 'start')
{
$tmpl_vars->{content} = 'start_page';
my $articles = [$sch->resultset('Article')->search(undef,
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
my $news = [$sch->resultset('News')->search(
{
is_put_on_main => 1
},
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
$tmpl_vars->{articles} = $articles;
$tmpl_vars->{news} = $news;
}
elsif ($act eq 'article')
{
$tmpl_vars->{content} = 'full_article';
$tmpl_vars->{article} = $sch->resultset('Article')->find({ id => $p{'id'} });
}
elsif ($act eq 'category')
{
$tmpl_vars->{content} = 'category';
$tmpl_vars->{category} = $sch->resultset('Category')->find({ id => $p{'id'} });
}
elsif ($act eq 'news')
{
$tmpl_vars->{content} = 'full_article';
$tmpl_vars->{article} = $sch->resultset('News')->find({ id => $p{'id'} });
}
else
{
# .
}
, , . , Perl , , HTML-CheckArgs HTML-QuickCheck . , HTML-Widget HTML-Tag . . , . .
: , . ( , error_action , ), :
print $q->header(-location => '?a=start');
exit;
, . , , ( ):
my %action = (
'start' => 'Main page',
'news' => 'News page',
'article' => 'Full article',
# ....
);
my $act = ( $p{'act'} && defined( $actions{$p{'act'}} )) ? $p{'act'} : 'start';
, — , 'start'. - defined(...) .
. , . , tmpl/admin .
: Digest::SHA1 CGI::Session . , — .
, . .
:
[%# login %]
[% IF err %]
Wrong login
[% END %]
/>
Login: />
Password: />
/>
, :
use CGI::Session;
use Digest::SHA1 qw(sha1_hex);
# ... CGI
my $s = CGI::Session->load(undef, undef, { Directory => 'ssss' } );
# ...
if ($s->empty && $act !~ /login(_form)?|logout/)
{
print $q->header(-location => '?a=login_form');
exit;
}
else
{
my $user = $sch->resultset('User')->find({ id => $s->param('uid') });
$tmpl_vars->{user} = $user;
}
if ($act eq 'login_form')
{
$tmpl_vars->{content} = 'login_form';
}
elsif ($act eq 'login')
{
unless (my $u = &login($p{'login'}, $p{'pass'}))
{
$tmpl_vars->{content} = 'login';
$tmpl_vars->{err} = 1;
}
else
{
$s = $s->new;
$s->param('uid', $u->id);
print $s->header(-location => '?a=start');
exit;
}
}
elsif ($act eq 'logout')
{
$s->delete;
print $q->header(-location => '?a=login');
exit;
}
#
sub login
{
my ($u, $p) = @_;
my $pp = sha1_hex($p);
my $res = $sch->resultset('User')->search({
name => $u,
pass => $pp
});
my $user = $res->next;
return $user;
}
, , .
CGI::Session , . — expired. ssss .
Digest::SHA1 - MD5.
. -, CRUD- (CReate, Update, Delete). , , DBIx::Class::WebForm . CRUD CPAN .
-, . FCKeditor , . .
-, . , DBIx::Class::Validation , , , CGI::FormBuilder , CGI::QuickForm .. "Form", "Validate" "Widget" .
"" . , , , . , , . , SQL-.
, - . .
-NOT_FOR_HOLYWARS-
Source: https://habr.com/ru/post/23543/
All Articles