├── web ├── robots.txt ├── favicon.ico ├── img │ ├── aperophp.gif │ ├── logo_apero_php.png │ ├── glyphicons-halflings.png │ ├── glyphicons-halflings-white.png │ └── jquery-ui │ │ ├── ui-icons_0073ea_256x240.png │ │ ├── ui-icons_454545_256x240.png │ │ ├── ui-icons_666666_256x240.png │ │ ├── ui-icons_ff0084_256x240.png │ │ ├── ui-icons_ffffff_256x240.png │ │ ├── ui-bg_flat_0_aaaaaa_40x100.png │ │ ├── ui-bg_flat_0_eeeeee_40x100.png │ │ ├── ui-bg_flat_55_ffffff_40x100.png │ │ ├── ui-bg_flat_75_ffffff_40x100.png │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ ├── ui-bg_highlight-soft_100_f6f6f6_1x100.png │ │ ├── ui-bg_highlight-soft_25_0073ea_1x100.png │ │ └── ui-bg_highlight-soft_50_dddddd_1x100.png ├── index.php ├── font │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff ├── js │ ├── datepicker.js │ ├── gmap.autocomplete.js │ └── stats.js └── css │ ├── apero.css │ └── app.css ├── composer.phar ├── Capfile ├── config ├── recipes │ ├── railsless.rb │ ├── app_config.rb │ ├── composer.rb │ └── database.rb └── deploy.rb ├── tests ├── units_db │ ├── bootstrap.php │ └── Aperophp │ │ ├── Repository │ │ ├── City.php │ │ ├── DrinkComment.php │ │ ├── Member.php │ │ ├── DrinkParticipant.php │ │ ├── User.php │ │ └── Drink.php │ │ ├── Lib │ │ └── Utils.php │ │ └── Provider │ │ └── Controller │ │ └── Comment.php └── units │ └── Aperophp │ └── Meetup │ ├── UserTransformer.php │ └── EventTransformer.php ├── Gemfile ├── src ├── Resources │ └── views │ │ ├── error │ │ └── default.html.twig │ │ ├── common │ │ ├── footer.html.twig │ │ ├── Form │ │ │ └── fields.html.twig │ │ ├── ga.html.twig │ │ ├── connection.html.twig │ │ └── topbar.html.twig │ │ ├── member │ │ ├── forget_mail.html.twig │ │ ├── remember_mail.html.twig │ │ ├── forget.html.twig │ │ ├── signin.html.twig │ │ ├── edit.html.twig │ │ └── signup.html.twig │ │ ├── drink │ │ ├── _preview.html.twig │ │ ├── participation_mail.html.twig │ │ ├── forget_mail.html.twig │ │ ├── invite_ics.twig │ │ ├── forget.html.twig │ │ ├── list.atom.twig │ │ ├── _participations.html.twig │ │ ├── list.html.twig │ │ ├── index.html.twig │ │ ├── _participate.html.twig │ │ ├── new.html.twig │ │ ├── edit.html.twig │ │ └── view.html.twig │ │ ├── comment │ │ ├── _list.html.twig │ │ └── _new.html.twig │ │ ├── layout.html.twig │ │ └── stats │ │ └── stats.html.twig └── Aperophp │ ├── Test │ ├── AkismetMock.php │ ├── Test.php │ └── Client.php │ ├── Validator │ └── Constraints │ │ ├── FutureDate.php │ │ └── FutureDateValidator.php │ ├── Meetup │ ├── UserTransformer.php │ └── EventTransformer.php │ ├── Provider │ ├── RepositoryServiceProvider.php │ ├── Error.php │ └── Controller │ │ ├── Stats.php │ │ └── Comment.php │ ├── Lib │ ├── Utils.php │ ├── MailFactory.php │ ├── AutoLinkTwigExtension.php │ └── Stats.php │ ├── Form │ ├── FormExtension.php │ ├── Type │ │ ├── ForgetMemberType.php │ │ ├── ForgetParticipationType.php │ │ ├── SigninType.php │ │ ├── EditMemberType.php │ │ ├── DrinkCommentType.php │ │ ├── SignupType.php │ │ ├── DrinkParticipationType.php │ │ └── DrinkType.php │ └── EventListener │ │ └── DataFilterSubscriber.php │ ├── Repository │ ├── Member.php │ ├── City.php │ ├── DrinkComment.php │ ├── User.php │ ├── DrinkParticipant.php │ ├── Repository.php │ └── Drink.php │ └── Command │ └── SyncWithMeetup.php ├── bin ├── tests.sh └── assets.sh ├── package.json ├── app ├── config_travis.php ├── app.php ├── config_test.php ├── config.php.dist ├── console └── bootstrap.php ├── .gitignore ├── .travis.yml ├── Gemfile.lock ├── .atoum.php ├── .atoum_db.php ├── assets ├── apero-responsive.less └── apero.less ├── data └── sql │ └── fixtures.sql ├── composer.json └── README.md /web/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /composer.phar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/composer.phar -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/favicon.ico -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | require "railsless-deploy" 2 | 3 | load 'deploy' 4 | load 'config/deploy' 5 | -------------------------------------------------------------------------------- /config/recipes/railsless.rb: -------------------------------------------------------------------------------- 1 | set :shared_children, [] 2 | set :public_children, [] 3 | -------------------------------------------------------------------------------- /tests/units_db/bootstrap.php: -------------------------------------------------------------------------------- 1 | run(); -------------------------------------------------------------------------------- /web/img/logo_apero_php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/logo_apero_php.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'capistrano', '~> 2.1' 4 | gem 'railsless-deploy' 5 | 6 | -------------------------------------------------------------------------------- /web/font/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/font/fontawesome-webfont.eot -------------------------------------------------------------------------------- /web/font/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/font/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /web/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /web/font/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/font/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/Resources/views/error/default.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% block content %} 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /web/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /web/img/jquery-ui/ui-icons_0073ea_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/jquery-ui/ui-icons_0073ea_256x240.png -------------------------------------------------------------------------------- /web/img/jquery-ui/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/jquery-ui/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /web/img/jquery-ui/ui-icons_666666_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/jquery-ui/ui-icons_666666_256x240.png -------------------------------------------------------------------------------- /web/img/jquery-ui/ui-icons_ff0084_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/jquery-ui/ui-icons_ff0084_256x240.png -------------------------------------------------------------------------------- /web/img/jquery-ui/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/jquery-ui/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /web/img/jquery-ui/ui-bg_flat_0_aaaaaa_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/jquery-ui/ui-bg_flat_0_aaaaaa_40x100.png -------------------------------------------------------------------------------- /web/img/jquery-ui/ui-bg_flat_0_eeeeee_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/jquery-ui/ui-bg_flat_0_eeeeee_40x100.png -------------------------------------------------------------------------------- /web/img/jquery-ui/ui-bg_flat_55_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/jquery-ui/ui-bg_flat_55_ffffff_40x100.png -------------------------------------------------------------------------------- /web/img/jquery-ui/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/jquery-ui/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /web/img/jquery-ui/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/jquery-ui/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /bin/tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | php app/console db:install --test --load-fixtures 4 | 5 | ./vendor/bin/atoum -c .atoum_db.php 6 | ./vendor/bin/atoum 7 | -------------------------------------------------------------------------------- /web/img/jquery-ui/ui-bg_highlight-soft_100_f6f6f6_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/jquery-ui/ui-bg_highlight-soft_100_f6f6f6_1x100.png -------------------------------------------------------------------------------- /web/img/jquery-ui/ui-bg_highlight-soft_25_0073ea_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/jquery-ui/ui-bg_highlight-soft_25_0073ea_1x100.png -------------------------------------------------------------------------------- /web/img/jquery-ui/ui-bg_highlight-soft_50_dddddd_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afup/aperophp/HEAD/web/img/jquery-ui/ui-bg_highlight-soft_50_dddddd_1x100.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aperophp", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "jshint": "^0.9.1", 6 | "recess": "1.1.9", 7 | "uglify-js": "^2.0.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/config_travis.php: -------------------------------------------------------------------------------- 1 | 'root', 7 | 'password' => '', 8 | ) 9 | ); 10 | -------------------------------------------------------------------------------- /src/Resources/views/common/footer.html.twig: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/Aperophp/Test/AkismetMock.php: -------------------------------------------------------------------------------- 1 | 3 | suivez ce lien pour obtenir un nouveau mot de passe 4 | 5 | -------------------------------------------------------------------------------- /config/recipes/app_config.rb: -------------------------------------------------------------------------------- 1 | namespace :deploy do 2 | task :set_app_config do 3 | app_config.map do |c| 4 | run "rm -f #{latest_release}/#{c} && ln -s #{shared_path}/config/#{c} #{latest_release}/#{c}" 5 | end 6 | end 7 | end 8 | 9 | after "deploy:finalize_update", "deploy:set_app_config" 10 | -------------------------------------------------------------------------------- /src/Resources/views/common/Form/fields.html.twig: -------------------------------------------------------------------------------- 1 | {% block field_errors %} 2 | {% spaceless %} 3 | {% for error in errors %} 4 | {{ error.messageTemplate|trans(error.messageParameters, 'validators') }} 5 | {% endfor %} 6 | {% endspaceless %} 7 | {% endblock field_errors %} -------------------------------------------------------------------------------- /src/Resources/views/member/remember_mail.html.twig: -------------------------------------------------------------------------------- 1 | Vous venez de demander la régénération de votre mot de passe.
2 | Vos identifiants sont :
3 | 7 | 8 | Vous pouvez vous connecter au site avec ceux-ci. 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Application files 2 | app/config.php 3 | bin/* 4 | deps.lock 5 | vendor/ 6 | node_modules/ 7 | tmp/* 8 | 9 | # IDE files 10 | # Netbeans 11 | nbproject/ 12 | # Eclipse 13 | .buildpath 14 | .project 15 | .settings/ 16 | # SublimeText 17 | *.sublime-* 18 | 19 | # Backup files 20 | *.*~ 21 | 22 | # Vagrant 23 | Vagrantfile 24 | .vagrant 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | env: 4 | - DB=mysql 5 | 6 | php: 7 | - 5.3 8 | - 5.4 9 | 10 | before_script: 11 | - wget -nc http://getcomposer.org/composer.phar 12 | - php composer.phar install --dev 13 | - mysql -e 'create database aperophp_test;' 14 | - cp app/config_travis.php app/config.php 15 | - php app/console db:install --test --load-fixtures 16 | 17 | script: ./vendor/bin/atoum -d tests/units 18 | 19 | sudo: false 20 | -------------------------------------------------------------------------------- /app/app.php: -------------------------------------------------------------------------------- 1 | mount(null, new Aperophp\Provider\Error()); 6 | $app->mount('/participation', new Aperophp\Provider\Controller\Participate()); 7 | $app->mount('/comment', new Aperophp\Provider\Controller\Comment()); 8 | $app->mount('/member', new Aperophp\Provider\Controller\Member()); 9 | $app->mount('/', new Aperophp\Provider\Controller\Stats()); 10 | $app->mount('/', new Aperophp\Provider\Controller\Drink()); 11 | 12 | return $app; 13 | -------------------------------------------------------------------------------- /web/js/datepicker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Init Datepicker. 3 | */ 4 | function init_datepicker(inputDP, altInputDP) 5 | { 6 | inputDP.datepicker({ 7 | firstDay: 1, 8 | dateFormat: 'dd/mm/yy', 9 | altField: altInputDP, 10 | altFormat: 'yy-mm-dd', 11 | dayNamesMin: ['Di','Lu','Ma','Me','Je','Ve','Sa'], 12 | monthNames: ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'], 13 | prevText: 'Mois précédent', 14 | nextText: 'Mois suivant', 15 | }); 16 | } -------------------------------------------------------------------------------- /app/config_test.php: -------------------------------------------------------------------------------- 1 | $app['db.options']['dbname'] . '_test', 12 | ) 13 | ); 14 | 15 | $app['swiftmailer.transport'] = new \Swift_Transport_NullTransport($app['swiftmailer.transport.eventdispatcher']); 16 | 17 | $app['akismet'] = $app->share(function() use ($app) { 18 | return new \Aperophp\Test\AkismetMock(); 19 | }); 20 | -------------------------------------------------------------------------------- /src/Resources/views/common/ga.html.twig: -------------------------------------------------------------------------------- 1 | {% if ga_enabled and ga_ua %} 2 | 11 | {% endif %} 12 | -------------------------------------------------------------------------------- /src/Resources/views/drink/_preview.html.twig: -------------------------------------------------------------------------------- 1 |
2 |
{{ drink.kind|trans }}
3 |

{{ drink.city_name }}

4 |

{{ drink.day|date("d") }} {{ drink.day|date("F")|trans|lower }} {{ drink.day|date("Y") }}

5 | 10 |

Voir le détail »

11 |
12 | -------------------------------------------------------------------------------- /src/Aperophp/Meetup/UserTransformer.php: -------------------------------------------------------------------------------- 1 | $rsvp['member']['member_id'], 22 | 'firstname' => $rsvp['member']['name'], 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/units_db/Aperophp/Repository/City.php: -------------------------------------------------------------------------------- 1 | assert 14 | ->if($cities = $this->app['cities']->findAllInAssociativeArray()) 15 | ->then 16 | ->boolean(is_array($cities))->isTrue() 17 | ->integer(count($cities))->isEqualTo(6) 18 | ; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | capistrano (2.15.5) 5 | highline 6 | net-scp (>= 1.0.0) 7 | net-sftp (>= 2.0.0) 8 | net-ssh (>= 2.0.14) 9 | net-ssh-gateway (>= 1.1.0) 10 | highline (1.6.21) 11 | net-scp (1.2.1) 12 | net-ssh (>= 2.6.5) 13 | net-sftp (2.1.2) 14 | net-ssh (>= 2.6.5) 15 | net-ssh (2.9.2) 16 | net-ssh-gateway (1.2.0) 17 | net-ssh (>= 2.6.5) 18 | railsless-deploy (1.1.3) 19 | 20 | PLATFORMS 21 | ruby 22 | 23 | DEPENDENCIES 24 | capistrano (~> 2.1) 25 | railsless-deploy 26 | -------------------------------------------------------------------------------- /src/Resources/views/common/connection.html.twig: -------------------------------------------------------------------------------- 1 | {% if app.session.has('member') %} 2 | 12 | {% endif %} 13 | -------------------------------------------------------------------------------- /src/Aperophp/Provider/RepositoryServiceProvider.php: -------------------------------------------------------------------------------- 1 | $class) { 17 | $app[$label] = $app->share(function($app) use ($class) { 18 | return new $class($app['db']); 19 | }); 20 | } 21 | } 22 | 23 | public function boot(Application $app) 24 | { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Resources/views/drink/participation_mail.html.twig: -------------------------------------------------------------------------------- 1 | Vous venez de vous inscrire à {{ drink.kind|trans }} le 2 | {{ drink.day|date("d") }} {{ drink.day|date("F")|trans|lower }} {{ drink.day|date("Y") }} 3 | à {{ drink.hour|date("H:i") }} à {{ drink.city_name }} qui se déroulera 4 | à {{ drink.place }}
5 | Détail
6 | Modifier sa participation
7 | Supprimer sa participation
8 | 9 | Votre jeton : {{ user.token }} 10 | -------------------------------------------------------------------------------- /src/Resources/views/drink/forget_mail.html.twig: -------------------------------------------------------------------------------- 1 | Voici le détail de votre participation à {{ drink.kind|trans }} le 2 | {{ drink.day|date("d") }} {{ drink.day|date("F")|trans|lower }} {{ drink.day|date("Y") }} 3 | à {{ drink.hour|date("H:i") }} à {{ drink.city_name }} qui se déroulera 4 | à {{ drink.place }}
5 | Détail
6 | Modifier sa participation
7 | Supprimer sa participation
8 | 9 | Votre jeton : {{ user.token }} 10 | -------------------------------------------------------------------------------- /src/Resources/views/drink/invite_ics.twig: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//AFUP//AperoPHP//EN\n 4 | CALSCALE:GREGORIAN 5 | METHOD:REQUEST 6 | BEGIN:VEVENT 7 | DTEND;VALUE=DATE-TIME:{{ datetimes.end }} 8 | STATUS:TENTATIVE 9 | DTSTART;VALUE=DATE-TIME:{{ datetimes.start }} 10 | TRANSP:TRANSPARENT 11 | DTSTAMP:{{ datetimes.current }} 12 | ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;X-NUM-GUESTS=0;CN={{ user.lastname }} {{ user.firstname }}:MAILTO:{{ user.email }} 13 | UID:{{ datetimes.current }}-aperophp.net 14 | SUMMARY:Apéro PHP 15 | ORGANIZER;CN=aperophp.net:MAILTO:noreply@aperophp.net 16 | LOCATION:{{ drink.address }} 17 | SEQUENCE:0 18 | DESCRIPTION:Détails sur l'apéro PHP : {{ url('_showdrink', {'id': drink.id}) }} 19 | END:VEVENT 20 | END:VCALENDAR 21 | 22 | -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | load "config/recipes/railsless" 4 | load "config/recipes/composer" 5 | load "config/recipes/database" 6 | load "config/recipes/railsless" 7 | 8 | set :app_config, %w(app/config.php) 9 | load "config/recipes/app_config" 10 | 11 | set :shared_children, ["app/log"] 12 | 13 | set :scm, :git 14 | set :repository, "https://github.com/afup/aperophp.git" 15 | set :deploy_via, :remote_cache 16 | set :branch, "master" 17 | 18 | set :keep_releases, 5 19 | set :use_sudo, false 20 | set :deploy_to, "/home/aperophp/aperophp.net" 21 | 22 | server 'aperophp.net', :app, :web, :db, :primary => true 23 | 24 | set :user, "aperophp" 25 | ssh_options[:forward_agent] = true 26 | 27 | after "deploy:restart", "deploy:cleanup" 28 | 29 | #TODO backup database 30 | -------------------------------------------------------------------------------- /src/Aperophp/Test/Test.php: -------------------------------------------------------------------------------- 1 | app = require __DIR__.'/../../../app/app.php'; 12 | require __DIR__.'/../../../app/config_test.php'; 13 | 14 | // Isolate DB 15 | $this->app['db']->beginTransaction(); 16 | } 17 | 18 | /** 19 | * Creates a Client. 20 | * 21 | * @param array $server An array of server parameters 22 | * 23 | * @return Client A Client instance 24 | */ 25 | public function createClient(array $server = array()) 26 | { 27 | return new Client($this->app, $server); 28 | } 29 | 30 | public function afterTestMethod($method) 31 | { 32 | $this->app['db']->rollback(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Resources/views/drink/forget.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |
8 |
9 | 10 | {{ form_widget(form.email, { 'attr': {'class': 'span2'} }) }} 11 | {{ form_errors(form.email) }} 12 |
13 |
14 | {{ form_rest(form) }} 15 |
16 | 17 |
18 |
19 |
20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /.atoum.php: -------------------------------------------------------------------------------- 1 | disableCodeCoverage(); 4 | $runner->addTestsFromDirectory(__DIR__ . '/tests/units'); 5 | 6 | ## Notifier (growlnotify) 7 | $images = __DIR__ . '/vendor/atoum/atoum/resources/images/logo'; 8 | 9 | $report = $script->AddDefaultReport(); 10 | 11 | if(syslibExist('growlnotify') ) 12 | { 13 | $notifier = new \mageekguy\atoum\report\fields\runner\result\notifier\image\growl(); 14 | $notifier 15 | ->setSuccessImage($images . DIRECTORY_SEPARATOR . 'success.png') 16 | ->setFailureImage($images . DIRECTORY_SEPARATOR . 'failure.png') 17 | ; 18 | $report->addField($notifier, array(atoum\runner::runStop)); 19 | } 20 | 21 | /** 22 | * Return true if library is available on system 23 | * 24 | * @param string $libName 25 | * @return boolean 26 | */ 27 | function syslibExist($libName) 28 | { 29 | return !is_null(shell_exec(sprintf('command -v %s 2>/dev/null', $libName))); 30 | } 31 | -------------------------------------------------------------------------------- /.atoum_db.php: -------------------------------------------------------------------------------- 1 | disableCodeCoverage(); 4 | $runner->addTestsFromDirectory(__DIR__ . '/tests/units_db'); 5 | 6 | ## Notifier (growlnotify) 7 | $images = __DIR__ . '/vendor/atoum/atoum/resources/images/logo'; 8 | 9 | $report = $script->AddDefaultReport(); 10 | 11 | if(syslibExist('growlnotify') ) 12 | { 13 | $notifier = new \mageekguy\atoum\report\fields\runner\result\notifier\image\growl(); 14 | $notifier 15 | ->setSuccessImage($images . DIRECTORY_SEPARATOR . 'success.png') 16 | ->setFailureImage($images . DIRECTORY_SEPARATOR . 'failure.png') 17 | ; 18 | $report->addField($notifier, array(atoum\runner::runStop)); 19 | } 20 | 21 | /** 22 | * Return true if library is available on system 23 | * 24 | * @param string $libName 25 | * @return boolean 26 | */ 27 | function syslibExist($libName) 28 | { 29 | return !is_null(shell_exec(sprintf('command -v %s 2>/dev/null', $libName))); 30 | } 31 | -------------------------------------------------------------------------------- /tests/units_db/Aperophp/Lib/Utils.php: -------------------------------------------------------------------------------- 1 | assert 26 | ->if($utils = new \Aperophp\Lib\Utils($this->app)) 27 | ->then 28 | ->string($utils->hash($string, $salt))->isEqualTo($hash) 29 | ; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Aperophp/Lib/Utils.php: -------------------------------------------------------------------------------- 1 | 9 | * @since 4 févr. 2012 10 | * @version 1.0 - 4 févr. 2012 - Koin 11 | */ 12 | class Utils 13 | { 14 | protected $app; 15 | 16 | public function __construct($app) 17 | { 18 | $this->app = $app; 19 | } 20 | 21 | /** 22 | * Hash my string. 23 | * 24 | * @param string $str The string to hash 25 | * @param string $salt The salt to use (if null, use app['secret']) 26 | * 27 | * @return string 28 | * 29 | * @author Koin 30 | * @since 4 févr. 2012 31 | * @version 1.1 - 4 févr. 2012 - Koin 32 | */ 33 | public function hash($str, $salt = null) 34 | { 35 | $salt = $salt ? $salt : $this->app['secret']; 36 | 37 | return sha1($str.$salt); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Aperophp/Validator/Constraints/FutureDateValidator.php: -------------------------------------------------------------------------------- 1 | context->addViolation($constraint->message, array('%date%' => $value)); 22 | } 23 | } 24 | 25 | if (is_string($value) && date('Y-m-d', strtotime($value)) < date('Y-m-d', time())) { 26 | $this->context->addViolation($constraint->message, array('%date%' => $value)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Aperophp/Form/FormExtension.php: -------------------------------------------------------------------------------- 1 | app = $app; 16 | } 17 | 18 | /** 19 | * {@inheritDoc} 20 | */ 21 | protected function loadTypes() 22 | { 23 | return array( 24 | new Type\DrinkCommentType($this->app['session']), 25 | new Type\DrinkParticipationType($this->app['session'], $this->app['drink_participants']), 26 | new Type\DrinkType($this->app['cities'], $this->app['drinks']), 27 | new Type\EditMemberType(), 28 | new Type\SigninType(), 29 | new Type\SignupType(), 30 | new Type\ForgetMemberType(), 31 | new Type\ForgetParticipationType(), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Aperophp/Test/Client.php: -------------------------------------------------------------------------------- 1 | request('GET', '/member/signin.html'); 12 | 13 | if (!$this->getResponse()->isOk()) { 14 | return false; 15 | } 16 | 17 | $form = $crawler->selectButton('login')->form(); 18 | 19 | $crawler = $this->submit($form, array( 20 | 'signin[username]' => $username, 21 | 'signin[password]' => $password, 22 | )); 23 | 24 | if (!$this->getResponse()->isRedirect('/')) { 25 | return false; 26 | } 27 | 28 | $crawler = $this->followRedirect(); 29 | 30 | if (1 !== $crawler->filter('a.dropdown-toggle:contains("Bienvenue, user")')->count()) { 31 | return false; 32 | } 33 | 34 | return true; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Resources/views/drink/list.atom.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AperoPHP 5 | Liste des apéroPHP à venir 6 | 7 | {{ "now"|date("c") }} 8 | 9 | AFUP 10 | contact@afup.org 11 | 12 | http://aperophp.net 13 | 14 | 15 | {% for drink in drinks %} 16 | 17 | {{ drink.city_name }} le {{ drink.day|date("l")|trans|lower }} {{ drink.day|date("d") }} {{ drink.day|date("F")|trans|lower }} {{ drink.day|date("Y") }} à {{ drink.day|date("H:i") }} 18 | 19 | {{ url('_showdrink', {'id': drink.id}) }} 20 | {{ drink.updated_at|date("c") }} 21 | {{ drink.description }} 22 | 23 | {% endfor %} 24 | 25 | -------------------------------------------------------------------------------- /src/Aperophp/Repository/Member.php: -------------------------------------------------------------------------------- 1 | db->fetchAssoc($sql, array($username, $password)); 25 | } 26 | 27 | /** 28 | * findOneByUsername 29 | * 30 | * @param string $username 31 | * 32 | * @return array 33 | */ 34 | public function findOneByUsername($username) 35 | { 36 | $sql = 'SELECT * FROM Member WHERE username = ?'; 37 | 38 | return $this->db->fetchAssoc($sql, array($username)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /web/css/apero.css: -------------------------------------------------------------------------------- 1 | /* 2 | Document : apero 3 | Created on : 21 janv. 2012, 15:47:49 4 | Author : shaim 5 | Description: 6 | Purpose of the stylesheet follows. 7 | */ 8 | 9 | /* 10 | TODO customize this sample style 11 | Syntax recommendation http://www.w3.org/TR/REC-CSS2/ 12 | */ 13 | 14 | root { 15 | display: block; 16 | } 17 | 18 | div.apero{ 19 | border: 1px #f9ab1a solid; 20 | padding: 1ex; 21 | border-top-left-radius: 6px; 22 | border-top-right-radius: 6px; 23 | border-bottom-right-radius: 6px; 24 | border-bottom-left-radius: 6px; 25 | } 26 | 27 | div.conference{ 28 | border: 1px #4c5e96 solid; 29 | padding: 1ex; 30 | border-top-left-radius: 6px; 31 | border-top-right-radius: 6px; 32 | border-bottom-right-radius: 6px; 33 | border-bottom-left-radius: 6px; 34 | } 35 | 36 | div.conference h5, 37 | div.apero h5{ 38 | background-color: #94a1c5; 39 | color: #fff; 40 | padding-left: 1ex; 41 | padding-right: 1ex; 42 | } 43 | 44 | .vmiddle { 45 | vertical-align: middle !important; 46 | } 47 | -------------------------------------------------------------------------------- /config/recipes/composer.rb: -------------------------------------------------------------------------------- 1 | # Add a recipe for Composer 2 | 3 | namespace :composer do 4 | desc "Runs composer to install vendors from composer.lock file" 5 | task :install, :roles => :app, :except => { :no_release => true } do 6 | php_bin = fetch :php_bin, "/usr/bin/php" 7 | composer_bin = fetch :composer_bin, "#{php_bin} composer.phar" 8 | composer_options = fetch :composer_options, "COMPOSER_PROCESS_TIMEOUT=4000" 9 | 10 | run "#{try_sudo} sh -c 'cd #{latest_release} && #{composer_options} #{composer_bin} install'" 11 | end 12 | 13 | task :copy_vendors, :except => { :no_release => true } do 14 | composer_vendor = fetch :composer_vendor, 'vendor' 15 | run "if [ ! -d #{latest_release}/#{composer_vendor} ]; then mkdir -p #{latest_release}/#{composer_vendor}; fi;" 16 | run "vendorDir=#{current_path}/#{composer_vendor}; if [ -d $vendorDir ] || [ -h $vendorDir ]; then cp -a $vendorDir/* #{latest_release}/#{composer_vendor}; fi;" 17 | end 18 | end 19 | 20 | before "composer:install", "composer:copy_vendors" 21 | after "deploy:finalize_update", "composer:install" 22 | -------------------------------------------------------------------------------- /assets/apero-responsive.less: -------------------------------------------------------------------------------- 1 | @media (max-width: 767px) { 2 | #main { 3 | padding: 0 10px; 4 | margin-right: -20px; 5 | margin-left: -20px; 6 | } 7 | .subnavbar { 8 | margin-left: -20px; 9 | margin-right: -20px; 10 | } 11 | .subnavbar-inner { 12 | height: auto; 13 | } 14 | .subnavbar .container > ul { 15 | width: 100%; 16 | height: auto; 17 | border: none; 18 | } 19 | .subnavbar .container > ul > li { 20 | width: 33%; 21 | height: 70px; 22 | margin-bottom: 0; 23 | border: none; 24 | } 25 | .subnavbar .container > ul > li.active > a { 26 | font-size: 11px; 27 | background: transparent; 28 | } 29 | .subnavbar .container > ul > li > a > i { 30 | display: inline-block; 31 | margin-bottom: 0; 32 | font-size: 20px; 33 | } 34 | } 35 | 36 | @media (max-width: 979px) { 37 | .navbar-fixed-top { 38 | position: static; 39 | margin-bottom: 0; 40 | } 41 | .subnavbar .container { 42 | width: auto; 43 | } 44 | } -------------------------------------------------------------------------------- /web/js/gmap.autocomplete.js: -------------------------------------------------------------------------------- 1 | // http://code.google.com/intl/fr/apis/maps/documentation/javascript/places.html#places_autocomplete 2 | var memoryPlace = ''; 3 | $(function() { 4 | var input = document.getElementById('drink_placegmap'); 5 | autocomplete = new google.maps.places.Autocomplete(input); 6 | google.maps.event.addListener(autocomplete, 'place_changed', function() { 7 | var place = autocomplete.getPlace(); 8 | $('#drink_place').val(place.name); 9 | $('#drink_address').val(place.formatted_address); 10 | $('#drink_latitude').val(place.geometry.location.lat().toFixed(5)); 11 | $('#drink_longitude').val(place.geometry.location.lng().toFixed(5)); 12 | $('#drink_place_disabled').val(place.name); 13 | $('#drink_address_disabled').val(place.formatted_address); 14 | memoryPlace = $("#drink_placegmap").val(); 15 | }); 16 | // Autoriser un RETURN ou le blur dans l'input avec l'autocomplete est déstabilisant 17 | $("#drink_placegmap").keypress(function(e){ 18 | if (e.keyCode == 13) { 19 | return false; 20 | } 21 | }); 22 | 23 | }); 24 | function checkMaps() { 25 | $("#drink_placegmap").val(memoryPlace); 26 | } 27 | -------------------------------------------------------------------------------- /app/config.php.dist: -------------------------------------------------------------------------------- 1 | 'pdo_mysql', 24 | 'host' => 'localhost', 25 | 'dbname' => 'aperophp', 26 | 'user' => '', 27 | 'password' => '', 28 | ); 29 | // ******* 30 | 31 | 32 | // ******* 33 | // ** SwiftMailer 34 | // ******* 35 | // @see http://silex.sensiolabs.org/doc/providers/swiftmailer.html#parameters 36 | // SMTP Transport 37 | $app['mail.options'] = array( 38 | 'host' => 'localhost', 39 | 'port' => '25', 40 | 'username' => '', 41 | 'password' => '', 42 | 'encryption' => null, 43 | 'auth_mode' => null, 44 | ); 45 | 46 | 47 | $app['akismet_api_key'] = 'api_key'; 48 | $app['akismet_url'] = 'http://localhost'; 49 | 50 | $app['ga.enabled'] = false; 51 | $app['ga.ua'] = ''; 52 | 53 | $app['meetup.api_key'] = ""; 54 | $app['meetup.groupurlnames'] = array( 55 | ); 56 | 57 | -------------------------------------------------------------------------------- /src/Aperophp/Provider/Error.php: -------------------------------------------------------------------------------- 1 | 13 | * @since 23 july 2012 14 | * @version 1.0 - 23 july 2012 - Gautier DI FOLCO 15 | */ 16 | class Error implements ControllerProviderInterface 17 | { 18 | public function connect(Application $app) 19 | { 20 | $controllers = $app['controllers_factory']; 21 | 22 | // ******* 23 | // ** Signin member 24 | // ******* 25 | 26 | $app->error(function (\Exception $e, $code) use($app) 27 | { 28 | $page = 'default'; 29 | switch ($code){ 30 | default: 31 | break; 32 | } 33 | $app['session']->getFlashBag()->add('error', $e->getMessage()); 34 | return new Response($app['twig']->render('error/'.$page.'.html.twig'), $code); 35 | }); 36 | // ******* 37 | 38 | return $controllers; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Resources/views/comment/_list.html.twig: -------------------------------------------------------------------------------- 1 | {% for comment in comments %} 2 |
3 |

 {{ comment.content|raw }}

4 | 5 | {% if comment.username is not null%} 6 | {{ comment.username }}, 7 | {% else %} 8 | {{ comment.firstname }}, 9 | {% endif %} 10 | le {{ comment.created_at|date("d") }} {{ comment.created_at|date("F")|trans|lower }} {{ comment.created_at|date("Y") }} à {{ comment.created_at|date("H:i") }} 11 | 12 | {% if display_spam_buttons %} 13 | {% if comment.is_spam %} 14 | Signaler comme non spam 15 | {% else %} 16 | Signaler comme spam 17 | {% endif %} 18 | {% endif %} 19 |
20 | {% endfor %} 21 | -------------------------------------------------------------------------------- /src/Aperophp/Repository/City.php: -------------------------------------------------------------------------------- 1 | findAll() as $city) { 26 | $cities[$city['id']] = $city['name']; 27 | } 28 | 29 | return $cities; 30 | } 31 | 32 | /** 33 | * 34 | * @return array 35 | */ 36 | public function findRecurrentInAssociativeArray() 37 | { 38 | $cities = array(); 39 | $sql = sprintf( 40 | 'SELECT c.id as id, c.name as name 41 | FROM Drink d, City c 42 | WHERE d.city_id = c.id 43 | GROUP BY c.id 44 | HAVING COUNT(d.id) > %s 45 | ORDER BY name 46 | ', Stats::RECURRENT_MINIMUM); 47 | 48 | foreach ($this->db->fetchAll($sql) as $city) { 49 | $cities[$city['id']] = $city['name']; 50 | } 51 | 52 | return $cities; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/Aperophp/Repository/DrinkComment.php: -------------------------------------------------------------------------------- 1 | db->fetchAll($sql, $params); 29 | } 30 | 31 | public function findOne($drinkId, $userId) 32 | { 33 | $sql = 'SELECT * FROM Drink_Comment WHERE drink_id = ? AND user_id = ? LIMIT 1'; 34 | 35 | return $this->db->fetchAssoc($sql, array((int) $drinkId, (int) $userId)); 36 | } 37 | 38 | public function groupByEmail($email, $userId) 39 | { 40 | $sql = 'UPDATE Drink_Comment SET user_id = ? WHERE user_id IN (SELECT id FROM User WHERE email = ?)'; 41 | 42 | $this->db->prepare($sql)->execute(array((int) $userId, $email)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/units_db/Aperophp/Repository/DrinkComment.php: -------------------------------------------------------------------------------- 1 | assert 14 | ->if($drinkComments = $this->app['drink_comments']->findByDrinkId(1)) 15 | ->then 16 | ->boolean(is_array($drinkComments))->isTrue() 17 | ->integer(count($drinkComments))->isEqualTo(2) 18 | ; 19 | 20 | foreach ($drinkComments as $drinkComment) { 21 | $this->assert 22 | ->boolean(is_array($drinkComment))->isTrue() 23 | ->boolean(array_key_exists('user_email', $drinkComment))->isTrue() 24 | ; 25 | } 26 | } 27 | 28 | public function testFindOne_withExistingEntry_returnArray() 29 | { 30 | $this->assert 31 | ->if($comment = $this->app['drink_comments']->findOne(1, 2)) 32 | ->then 33 | ->boolean(is_array($comment))->isTrue() 34 | ; 35 | } 36 | 37 | public function testFindOne_withInexistingEntry_returnFalse() 38 | { 39 | $this->assert 40 | ->if($comment = $this->app['drink_comments']->findOne(231, 2341)) 41 | ->then 42 | ->boolean($comment)->isFalse() 43 | ; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Resources/views/member/forget.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 | 8 |

Mot de passe égaré

9 |
10 |
11 |
12 |
13 |
14 | 15 |
16 |
17 | 18 | {{ form_widget(form.email, { 'attr': {'class': 'span2'} }) }} 19 | {{ form_errors(form.email) }} 20 |
21 |
22 |
23 |
24 | {{ form_rest(form) }} 25 |
26 | 27 |
28 |
29 |
30 |
31 |
32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /src/Aperophp/Form/Type/ForgetMemberType.php: -------------------------------------------------------------------------------- 1 | 15 | * @since 28 july 2012 16 | * @version 1.0 - 28 july. 2012 - Gautier DI FOLCO 17 | */ 18 | class ForgetMemberType extends AbstractType 19 | { 20 | public function buildForm(FormBuilderInterface $builder, array $options) 21 | { 22 | $builder->addEventSubscriber(new DataFilterSubscriber($builder)); 23 | 24 | $builder->add('email', 'email'); 25 | } 26 | 27 | public function setDefaultOptions(OptionsResolverInterface $resolver) 28 | { 29 | $collectionConstraint = new Constraints\Collection(array( 30 | 'fields' => array( 31 | 'email' => array( 32 | new Constraints\NotBlank(), 33 | new Constraints\Email(), 34 | ), 35 | ), 36 | 'allowExtraFields' => false, 37 | )); 38 | 39 | $resolver->setDefaults(array( 40 | 'validation_constraint' => $collectionConstraint 41 | )); 42 | } 43 | 44 | public function getName() 45 | { 46 | return 'member_forget'; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Aperophp/Form/Type/ForgetParticipationType.php: -------------------------------------------------------------------------------- 1 | 15 | * @since 28 july 2012 16 | * @version 1.0 - 28 july. 2012 - Gautier DI FOLCO 17 | */ 18 | class ForgetParticipationType extends AbstractType 19 | { 20 | public function buildForm(FormBuilderInterface $builder, array $options) 21 | { 22 | $builder->addEventSubscriber(new DataFilterSubscriber($builder)); 23 | 24 | $builder->add('email', 'email'); 25 | } 26 | 27 | public function setDefaultOptions(OptionsResolverInterface $resolver) 28 | { 29 | $collectionConstraint = new Constraints\Collection(array( 30 | 'fields' => array( 31 | 'email' => array( 32 | new Constraints\NotBlank(), 33 | new Constraints\Email(), 34 | ), 35 | ), 36 | 'allowExtraFields' => false, 37 | )); 38 | 39 | $resolver->setDefaults(array( 40 | 'validation_constraint' => $collectionConstraint 41 | )); 42 | } 43 | 44 | public function getName() 45 | { 46 | return 'participation_forget'; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /web/css/app.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 480px) { 2 | .modal.fade.in { 3 | left: -20px; 4 | right: -20px; 5 | } 6 | .widget { 7 | overflow: visible; 8 | } 9 | } 10 | 11 | span.highlighted-number { 12 | font-weight:bold; 13 | font-size:35px; 14 | } 15 | 16 | #map { 17 | height: 350px; 18 | } 19 | 20 | .leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { 21 | -webkit-transition: -webkit-transform 0.2s ease-out, opacity 0.2s ease-in; 22 | -moz-transition: -moz-transform 0.2s ease-out, opacity 0.2s ease-in; 23 | -o-transition: -o-transform 0.2s ease-out, opacity 0.2s ease-in; 24 | transition: transform 0.2s ease-out, opacity 0.2s ease-in; 25 | } 26 | 27 | .marker-cluster-small { 28 | background-color: rgba(181, 226, 140, 0.6); 29 | } 30 | .marker-cluster-small div { 31 | background-color: rgba(110, 204, 57, 0.6); 32 | } 33 | 34 | .marker-cluster-medium { 35 | background-color: rgba(241, 211, 87, 0.6); 36 | } 37 | .marker-cluster-medium div { 38 | background-color: rgba(240, 194, 12, 0.6); 39 | } 40 | 41 | .marker-cluster-large { 42 | background-color: rgba(253, 156, 115, 0.6); 43 | } 44 | .marker-cluster-large div { 45 | background-color: rgba(241, 128, 23, 0.6); 46 | } 47 | 48 | .marker-cluster { 49 | background-clip: padding-box; 50 | border-radius: 20px; 51 | } 52 | .marker-cluster div { 53 | width: 30px; 54 | height: 30px; 55 | margin-left: 5px; 56 | margin-top: 5px; 57 | 58 | text-align: center; 59 | border-radius: 15px; 60 | font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; 61 | } 62 | .marker-cluster span { 63 | line-height: 30px; 64 | } 65 | -------------------------------------------------------------------------------- /src/Aperophp/Repository/User.php: -------------------------------------------------------------------------------- 1 | db->fetchAssoc($sql, array((int) $memberId)); 24 | } 25 | 26 | /** 27 | * findOneByEmailToken 28 | * 29 | * @param mixed $email 30 | * @param mixed $token 31 | * 32 | * @return array 33 | */ 34 | public function findOneByEmailToken($email, $token) 35 | { 36 | $sql = 'SELECT * FROM User WHERE email = ? AND token = ? LIMIT 1'; 37 | 38 | return $this->db->fetchAssoc($sql, array($email, $token)); 39 | } 40 | 41 | /** 42 | * findOneByEmail 43 | * 44 | * @param mixed $email 45 | * 46 | * @return array 47 | */ 48 | public function findOneByEmail($email) 49 | { 50 | $sql = 'SELECT * FROM User WHERE email = ? LIMIT 1'; 51 | 52 | return $this->db->fetchAssoc($sql, array($email)); 53 | } 54 | 55 | /** 56 | * removeUsers 57 | * 58 | * @param mixed $email 59 | * @param integer $userId 60 | */ 61 | public function removeUsers($email, $userId) 62 | { 63 | $sql = 'DELETE FROM User WHERE id <> ? AND email = ?'; 64 | 65 | $this->db->prepare($sql)->execute(array((int) $userId, $email)); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/Resources/views/drink/_participations.html.twig: -------------------------------------------------------------------------------- 1 | {% if not isFinished and not drink.meetup_com_id %} 2 |
3 | {% if isParticipating %} 4 | Modifier 5 | Se désinscrire 6 | {% else %} 7 | S'inscrire 8 | {% if not isConnected %} 9 | Jeton perdu ? 10 | {% endif %} 11 | {% endif %} 12 |
13 | 14 | 17 | {% endif %} 18 | 19 |

Total : {{ nb }}

20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for participant in participants %} 30 | 31 | {% if participant.member_id is not null%} 32 | 33 | {% else %} 34 | 35 | {% endif %} 36 | 37 | 38 | {% endfor %} 39 | 40 |
ParticipantPrésence
 {{ participant.username }} {{ participant.firstname }}{{ presences[participant.percentage]|trans }}
41 | -------------------------------------------------------------------------------- /src/Resources/views/drink/list.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 | 8 |

Liste des apéros

9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for drink in drinks %} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% endfor %} 33 | 34 |
QuandPourOrganisateurNombre
Le {{ drink.day|date("d") }} {{ drink.day|date("F")|trans|lower }} {{ drink.day|date("Y") }}, à {{ drink.hour|date("H:i") }}A {{ drink.city_name }}, {{ drink.place }}{{ drink.kind|trans }}{{ drink.organizer_username }}{{ drink.participants_count }}Voir le détail »
35 |
36 |
37 |
38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /src/Aperophp/Lib/MailFactory.php: -------------------------------------------------------------------------------- 1 | mailer = $mailer; 13 | $this->twig = $twig; 14 | } 15 | 16 | public function createParticipation($user, $drink) 17 | { 18 | $dDrink = \Datetime::createFromFormat('Y-m-d H:i:s', $drink['day'] . ' ' . $drink['hour']); 19 | 20 | $dEndDrink = clone $dDrink; 21 | $dEndDrink->modify('+3 hours'); 22 | $dateFormat = 'Ymd\THis'; 23 | 24 | 25 | $icsInvite = \Swift_Attachment::newInstance() 26 | ->setContentType('text/calendar;charset=UTF-8;method=REQUEST') 27 | ->setBody($this->twig->render('drink/invite_ics.twig', array( 28 | 'user' => $user, 29 | 'drink' => $drink, 30 | 'datetimes' => array( 31 | 'start' => $dDrink->format($dateFormat), 32 | 'end' => $dEndDrink->format($dateFormat), 33 | 'current' => date($dateFormat), 34 | ), 35 | ))) 36 | ->setEncoder(\Swift_Encoding::getQpEncoding()) 37 | ; 38 | 39 | return $this->mailer 40 | ->createMessage() 41 | ->setSubject('[Aperophp.net] Inscription à un '.$drink['kind']) 42 | ->setFrom(array('noreply@aperophp.net')) 43 | ->setTo(array($user['email'])) 44 | ->setBody($this->twig->render('drink/participation_mail.html.twig', array( 45 | 'user' => $user, 46 | 'drink' => $drink 47 | )), 'text/html') 48 | ->attach($icsInvite) 49 | ; 50 | } 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/Aperophp/Form/Type/SigninType.php: -------------------------------------------------------------------------------- 1 | 15 | * @since 4 févr. 2012 16 | * @version 1.0 - 4 févr. 2012 - Koin 17 | */ 18 | class SigninType extends AbstractType 19 | { 20 | public function buildForm(FormBuilderInterface $builder, array $options) 21 | { 22 | $builder->addEventSubscriber(new DataFilterSubscriber($builder)); 23 | 24 | $builder 25 | ->add('username', 'text', array('label' => 'Identifiant')) 26 | ->add('password', 'password', array('label' => 'Mot de passe')); 27 | } 28 | 29 | public function setDefaultOptions(OptionsResolverInterface $resolver) 30 | { 31 | $collectionConstraint = new Constraints\Collection(array( 32 | 'fields' => array( 33 | 'username' => array( 34 | new Constraints\Length(array('max' => 80)), 35 | new Constraints\NotNull(), 36 | ), 37 | 'password' => array( 38 | new Constraints\Length(array('max' => 80)), 39 | new Constraints\NotNull(), 40 | ), 41 | ), 42 | 'allowExtraFields' => false, 43 | )); 44 | 45 | $resolver->setDefaults(array( 46 | 'validation_constraint' => $collectionConstraint 47 | )); 48 | } 49 | 50 | public function getName() 51 | { 52 | return 'signin'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Resources/views/member/signin.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 | 8 |

Connexion

9 |
10 |
11 |
12 |
13 |
14 | 15 |
16 | {{ form_widget(form.username) }} 17 | {{ form_errors(form.username) }} 18 |
19 |
20 |
21 | 22 |
23 | {{ form_widget(form.password) }} 24 | {{ form_errors(form.password) }} 25 |
26 |
27 |
28 | {{ form_rest(form) }} 29 |
30 | 31 |
32 |
33 |
34 |
35 |
36 | 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /src/Aperophp/Repository/DrinkParticipant.php: -------------------------------------------------------------------------------- 1 | db->fetchAssoc($sql, array((int) $drinkId, (int) $userId)); 18 | } 19 | 20 | public function findByDrinkId($drinkId) 21 | { 22 | $sql = ' 23 | SELECT u.*, p.percentage as percentage, 24 | (SELECT username FROM Member m WHERE m.id = u.member_id) as username 25 | FROM User u, Drink_Participation p 26 | WHERE p.user_id = u.id 27 | AND p.drink_id = ? 28 | ORDER BY percentage DESC 29 | '; 30 | 31 | return $this->db->fetchAll($sql, array((int) $drinkId)); 32 | } 33 | 34 | public function findDrinksByUserId($userId) 35 | { 36 | $sql = 'SELECT d.* FROM Drink d, Drink_Participation p WHERE p.drink_id = d.id AND user_id = ?'; 37 | 38 | return $this->db->fetchAll($sql, array((int) $userId)); 39 | } 40 | 41 | public function findAllPresencesInAssociativeArray() 42 | { 43 | return array( 44 | 100 => 'For sure, I will be there', 45 | 70 => 'I will probably be there', 46 | 30 => 'I will try to be there', 47 | 0 => 'I won\'t be there', 48 | ); 49 | } 50 | 51 | public function groupByEmail($email, $userId) 52 | { 53 | $sql = 'UPDATE Drink_Participation SET user_id = ? WHERE user_id IN (SELECT id FROM User WHERE email = ?)'; 54 | 55 | $this->db->prepare($sql)->execute(array((int) $userId, $email)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /config/recipes/database.rb: -------------------------------------------------------------------------------- 1 | namespace :database do 2 | namespace :mysql do 3 | desc <<-DESC 4 | Create a compressed backup of given mysql database. 5 | This task must not be called directly because it need :db_config variable who contains 6 | database connection informations 7 | DESC 8 | task :backup, :roles => :app, :only => { :primary => true } do 9 | database_dump = fetch :database_dump, "mysqldump" 10 | gzip = fetch :gzip, "gzip" 11 | backup_path = fetch :backup_path, File.join(deploy_to, 'backup') 12 | 13 | # Execute database backup, using release name as archive name to easily association release and dump 14 | run "#{database_dump} --host=#{db_config['host']} --user=#{db_config['user']} --password #{db_config['dbname']} | #{gzip} > #{backup_path}/#{release_name}.sql.gz" do |ch, stream, out| 15 | # Password will not be displayed in console or store in history 16 | ch.send_data "#{db_config['password']}\n" if out =~ /^Enter password:/ 17 | end 18 | end 19 | end 20 | 21 | desc "Clean databases backups to keep only last backups" 22 | task :cleanup do 23 | backup_path = fetch :backup_path, File.join(deploy_to, 'backup') 24 | 25 | # Code duplicate from deploy:cleanup task (and adapted for databases backup) 26 | count = fetch(:keep_releases, 5).to_i 27 | local_backup = capture("ls -xt #{backup_path}").split.reverse 28 | if count >= local_backup.length 29 | logger.important "no old database backup to clean up" 30 | else 31 | logger.info "keeping #{count} of #{local_backup.length} database backup" 32 | directories = (local_backup - local_backup.last(count)).map { |release| 33 | File.join(backup_path, release) }.join(" ") 34 | 35 | try_sudo "rm -rf #{directories}" 36 | end 37 | end 38 | end 39 | 40 | after "deploy:cleanup", "database:cleanup" 41 | 42 | -------------------------------------------------------------------------------- /src/Aperophp/Form/EventListener/DataFilterSubscriber.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class DataFilterSubscriber implements EventSubscriberInterface 20 | { 21 | protected $builder; 22 | 23 | public function __construct(FormBuilderInterface $builder) 24 | { 25 | $this->builder = $builder; 26 | } 27 | 28 | public static function getSubscribedEvents() 29 | { 30 | return array(FormEvents::PRE_SET_DATA => 'preSetData'); 31 | } 32 | 33 | public function preSetData(FilterDataEvent $event) 34 | { 35 | $data = $event->getData(); 36 | 37 | if (null === $data) { 38 | return; 39 | } 40 | 41 | $data = $this->filterData($data); 42 | 43 | $event->setData($data); 44 | } 45 | 46 | protected function filterData($data, $builder = null) 47 | { 48 | if (null == $builder) { 49 | $builder = $this->builder; 50 | } 51 | 52 | $filteredData = array(); 53 | foreach ($data as $key => $value) { 54 | if (!$builder->has($key)) { 55 | continue; 56 | } 57 | 58 | if (is_array($value)) { 59 | $filteredData[$key] = $this->filterData($data[$key], $builder->get($key)); 60 | } else { 61 | $filteredData[$key] = $data[$key]; 62 | } 63 | } 64 | 65 | return $filteredData; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /data/sql/fixtures.sql: -------------------------------------------------------------------------------- 1 | SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; 2 | SET time_zone = "+00:00"; 3 | 4 | -- 5 | -- Contenu de la table `City` 6 | -- 7 | 8 | INSERT INTO `City` (`name`) VALUES 9 | ('Bordeaux'), 10 | ('Lyon'), 11 | ('Nantes'), 12 | ('Orléans'), 13 | ('Paris'), 14 | ('Toulouse'); 15 | 16 | -- 17 | -- Contenu de la table `Member` (mdp is "password") 18 | -- 19 | 20 | INSERT INTO `Member` (`id`, `username`, `password`, `active`) VALUES 21 | (1, 'user', '1d85bd100e0dd11b20f67a5834c8c2d67e7d9720', true), 22 | (2, 'user2', '1d85bd100e0dd11b20f67a5834c8c2d67e7d9720', true), 23 | (3, 'inactive_user', '1d85bd100e0dd11b20f67a5834c8c2d67e7d9720', false); 24 | 25 | -- 26 | -- Contenu de la table `User` 27 | -- 28 | 29 | INSERT INTO `User` (`id`, `lastname`, `firstname`, `email`, `token`, `member_id`) VALUES 30 | (1, 'Example1', 'User1', 'user1@example.org', 'token', null), 31 | (2, 'Example2', 'User2', 'user2@example.org', 'token', null), 32 | (3, 'Example3', 'User3', 'user3@example.org', 'token', 1), 33 | (4, 'Example4', 'User4', 'user4@example.org', 'token', null); 34 | 35 | -- 36 | -- Contenu de la table `Drink` 37 | -- 38 | 39 | INSERT INTO `Drink` (`id`, `place`, `address`, `day`, `hour`, `kind`, `description`, `member_id`, `city_id`, `latitude`, `longitude`) VALUES 40 | (1, 'Au père tranquille', '16 rue Pierre Lescot, Paris, France', '2016-07-19', '19:30:00', 'drink', 'Apéro PHP de test au père tranquille', 1, 5, '48.86214', '2.34843'), 41 | (2, 'Au père tranquille', '16 rue Pierre Lescot, Paris, France', '2010-07-19', '19:30:00', 'drink', 'Apéro déjà passé.', 1, 5, '48.86214', '2.34843'); 42 | 43 | -- 44 | -- Contenu de la table `Drink_Participation` 45 | -- 46 | 47 | INSERT INTO `Drink_Participation` (`drink_id`, `user_id`, `percentage`, `reminder`) VALUES 48 | (1, 1, 70, 1), 49 | (1, 3, 70, 1), 50 | (1, 4, 70, 1); 51 | 52 | -- 53 | -- Contenu de la table `Drink_Comment` 54 | -- 55 | 56 | INSERT INTO `Drink_Comment` (`id`, `created_at`, `content`, `drink_id`, `user_id`) VALUES 57 | (1, '2012-07-03 21:56:06', 'c\'est génial !', 1, 2), 58 | (2, '2012-07-03 21:57:17', 'Je suis bien d\'accord.', 1, 4); 59 | -------------------------------------------------------------------------------- /tests/units_db/Aperophp/Repository/Member.php: -------------------------------------------------------------------------------- 1 | assert 14 | ->if($member = $this->app['members']->findOneByUsernameAndPassword('user', $this->app['utils']->hash('password'))) 15 | ->then 16 | ->boolean(is_array($member))->isTrue() 17 | ; 18 | } 19 | 20 | public function testFindOneByUsernameAndPassword_withIncorrectPassword_returnFalse() 21 | { 22 | $this->assert 23 | ->if($member = $this->app['members']->findOneByUsernameAndPassword('user', 'wrong-password')) 24 | ->then 25 | ->boolean($member)->isFalse() 26 | ; 27 | } 28 | 29 | public function testFindOneByUsernameAndPassword_withInactiveUser_returnFalse() 30 | { 31 | $this->assert 32 | ->if($member = $this->app['members']->findOneByUsernameAndPassword('inactive-user', $this->app['utils']->hash('password'))) 33 | ->then 34 | ->boolean($member)->isFalse() 35 | ; 36 | } 37 | 38 | public function testFindOneByUsername_withExistingEntry_returnArray() 39 | { 40 | $this->assert 41 | ->if($member = $this->app['members']->findOneByUsername('user')) 42 | ->then 43 | ->boolean(is_array($member))->isTrue() 44 | ; 45 | } 46 | 47 | public function testFindOneByUsername_withIncorrectUsername_returnFalse() 48 | { 49 | $this->assert 50 | ->if($member = $this->app['members']->findOneByUsername('no-exist-user')) 51 | ->then 52 | ->boolean($member)->isFalse() 53 | ; 54 | } 55 | 56 | public function testFindOneByUsername_withInactiveUser_returnArray() 57 | { 58 | $this->assert 59 | ->if($member = $this->app['members']->findOneByUsername('inactive_user')) 60 | ->then 61 | ->boolean(is_array($member))->isTrue() 62 | ; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Aperophp/Meetup/EventTransformer.php: -------------------------------------------------------------------------------- 1 | cities = $cities; 18 | } 19 | 20 | /** 21 | * @param array $event 22 | * 23 | * @return array 24 | * 25 | * @throws \Exception 26 | */ 27 | public function transform(array $event) 28 | { 29 | if (!$this->isValid($event)) { 30 | return null; 31 | } 32 | 33 | $time = $this->transformDate($event['time']); 34 | 35 | if (false === ($cityId = array_search($event['venue']['city'], $this->cities))) { 36 | throw new \Exception(sprintf("City %s not found", var_export($event['venue']['city'], true))); 37 | } 38 | 39 | return array( 40 | 'city_id' => $cityId, 41 | 'place' => $event['venue']['address_1'], 42 | 'address' => $event['venue']['address_1'] . ' ' . $event['venue']['city'] . ', ' . $event['venue']['localized_country_name'], 43 | 'latitude' => $event['venue']['lat'], 44 | 'longitude' => $event['venue']['lon'], 45 | 'description' => $event['description'], 46 | 'day' => $time->format('Y-m-d'), 47 | 'hour' => $time->format('H:i'), 48 | 'created_at' => $this->transformDate($event['created'])->format('Y-m-d H:i:s'), 49 | 'updated_at' => $this->transformDate($event['updated'])->format('Y-m-d H:i:s'), 50 | 'meetup_com_id' => $event['id'], 51 | 'meetup_com_event_url' => $event['event_url'], 52 | ); 53 | } 54 | 55 | /** 56 | * @param array $event 57 | * 58 | * @return bool 59 | */ 60 | protected function isValid(array $event) 61 | { 62 | return false !== stripos($event['name'], "Apéro"); 63 | } 64 | 65 | /** 66 | * @param string $date 67 | * 68 | * @return \DateTime 69 | */ 70 | protected function transformDate($date) 71 | { 72 | $updated = \DateTime::createFromFormat('U', $date / 1000, new \DateTimeZone('UTC')); 73 | $updated->setTimezone(new \DateTimeZone('Europe/Paris')); 74 | 75 | return $updated; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /web/js/stats.js: -------------------------------------------------------------------------------- 1 | jQuery(document).ready(function() { 2 | Highcharts.setOptions({ 3 | lang: { 4 | months: ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 5 | 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'], 6 | weekdays: ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 7 | 'Jeudi', 'Vendredi', 'Samedi'], 8 | shortMonths: ['Jan', 'Fev', 'Mar', 'Avr', 'Mai', 'Juin', 'Juil', 9 | 'Aout', 'Sept', 'Oct', 'Nov', 'Déc'], 10 | decimalPoint: ',', 11 | downloadPNG: 'Télécharger en image PNG', 12 | downloadJPEG: 'Télécharger en image JPEG', 13 | downloadPDF: 'Télécharger en document PDF', 14 | downloadSVG: 'Télécharger en document Vectoriel', 15 | exportButtonTitle: 'Export du graphique', 16 | loading: 'Chargement en cours...', 17 | printButtonTitle: 'Imprimer le graphique', 18 | resetZoom: 'Réinitialiser le zoom', 19 | resetZoomTitle: 'Réinitialiser le zoom au niveau 1:1', 20 | thousandsSep: ' ', 21 | decimalPoint: ',' 22 | } 23 | }); 24 | $('table.highchart') 25 | .bind('highchartTable.beforeRender', function(event, highChartConfig) { 26 | highChartConfig.chart.zoomType = "x"; 27 | highChartConfig.xAxis.dateTimeLabelFormats = { 28 | millisecond: '%H:%M:%S.%L', 29 | second: '%H:%M:%S', 30 | minute: '%H:%M', 31 | hour: '%H:%M', 32 | day: '%e. %b', 33 | week: '%e. %b', 34 | month: '%b \'%y', 35 | year: '%Y' 36 | }; 37 | }) 38 | .highchartTable(); 39 | 40 | 41 | var latlng = L.latLng(46.70, 2.51); 42 | var map = L.map('map', {center: latlng, zoom: 5, maxZoom: 8, minZoom: 4, fullscreenControl: true}); 43 | L.tileLayer.provider('OpenStreetMap').addTo(map); 44 | 45 | var markers = L.markerClusterGroup({maxClusterRadius:50, singleMarkerMode:true}); 46 | 47 | for (var i = 0; i < addressPoints.length; i++) { 48 | var a = addressPoints[i]; 49 | var title = a[2]; 50 | var marker = L.marker(new L.LatLng(a[0], a[1]), { title: title }); 51 | marker.bindPopup(title); 52 | markers.addLayer(marker); 53 | } 54 | 55 | map.addLayer(markers); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/units_db/Aperophp/Repository/DrinkParticipant.php: -------------------------------------------------------------------------------- 1 | assert 14 | ->if($participation = $this->app['drink_participants']->findOne(1, 1)) 15 | ->then 16 | ->boolean(is_array($participation))->isTrue() 17 | ; 18 | } 19 | 20 | public function testFindOne_withInexistingEntry_returnFalse() 21 | { 22 | $this->assert 23 | ->if($participation = $this->app['drink_participants']->findOne(231, 2341)) 24 | ->then 25 | ->boolean($participation)->isFalse() 26 | ; 27 | } 28 | 29 | public function testFindByDrinkId_withExistingEntry_returnArray() 30 | { 31 | $this->assert 32 | ->if($participation = $this->app['drink_participants']->findByDrinkId(1)) 33 | ->then 34 | ->if(is_array($participation)) 35 | ->then 36 | ->boolean(3 == count($participation))->isTrue() 37 | ; 38 | } 39 | 40 | public function testFindByDrinkId_withUknownEntry_returnArray() 41 | { 42 | $this->assert 43 | ->if($participation = $this->app['drink_participants']->findByDrinkId(42)) 44 | ->then 45 | ->if(is_array($participation)) 46 | ->then 47 | ->boolean(0 == count($participation))->isTrue() 48 | ; 49 | } 50 | 51 | public function testFindByUserId_withExistingEntry_returnArray() 52 | { 53 | $this->assert 54 | ->if($participation = $this->app['drink_participants']->findDrinksByUserId(1)) 55 | ->then 56 | ->if(is_array($participation)) 57 | ->then 58 | ->boolean(1 == count($participation))->isTrue() 59 | ; 60 | } 61 | 62 | public function testFindByUserId_withUknownEntry_returnArray() 63 | { 64 | $this->assert 65 | ->if($participation = $this->app['drink_participants']->findDrinksByUserId(42)) 66 | ->then 67 | ->if(is_array($participation)) 68 | ->then 69 | ->boolean(0 == count($participation))->isTrue() 70 | ; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Aperophp/Provider/Controller/Stats.php: -------------------------------------------------------------------------------- 1 | match('stats_{type}-{city}.html', function(Request $request, $type, $city) use ($app) 18 | { 19 | $app['session']->set('menu', 'stats'); 20 | 21 | $types = StatsLib::getTypes(); 22 | 23 | if (!isset($types[$type])) { 24 | $type = 'all'; 25 | } 26 | 27 | $dateFrom = StatsLib::getDateFrom($type); 28 | 29 | 30 | $stats = new StatsLib($app['db'], $dateFrom, $city); 31 | $totalCount = $stats->getCount(); 32 | 33 | 34 | $geo = array(); 35 | foreach ($stats->getGeoInformations() as $info) { 36 | $geo[] = array($info['latitude'], $info['longitude'], $info['description']); 37 | } 38 | 39 | $displayedDate = $dateFrom; 40 | if (count($first = $stats->findFirst())) { 41 | $displayedDate = $first['day']; 42 | } 43 | 44 | $cities = array(City::ALL => 'Toutes') + $app['cities']->findRecurrentInAssociativeArray(); 45 | 46 | $various = array( 47 | 'month' => $stats->getMostUsedMonth(), 48 | 'average' => $stats->getAverageParticipants(), 49 | 'max' => $stats->getMaxParticipants(), 50 | ); 51 | 52 | return $app['twig']->render('stats/stats.html.twig', array( 53 | 'total' => $totalCount, 54 | 'total_participants' => $stats->countAllParticipants(), 55 | 'avg_participants' => $stats->averageParticipantsByCity('all' == $type), 56 | 'date_participants' => $stats->countParticipantsByDate(), 57 | 'date_from' => $displayedDate, 58 | 'geo' => $geo, 59 | 'type' => $type, 60 | 'types' => $types, 61 | 'city' => $city, 62 | 'cities' => $cities, 63 | 'display_all_cities' => $city == City::ALL, 64 | 'various' => $various 65 | )); 66 | }) 67 | ->bind('_stats') 68 | ->method('GET'); 69 | 70 | return $controllers; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Resources/views/comment/_new.html.twig: -------------------------------------------------------------------------------- 1 | {% form_theme commentForm 'common/Form/fields.html.twig' %} 2 | 3 |
4 | 8 | 42 | 46 |
47 | -------------------------------------------------------------------------------- /src/Resources/views/drink/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 |
8 | 9 |

A venir

10 |
11 |
12 | {% if drinks %} 13 |
    14 | {% for drink in drinks %} 15 |
  • 16 |
    17 | {{ drink.kind|trans|upper }} - {{ drink.city_name }} 18 |

    Info : {{ drink.description|autolink(1, 4)|truncate(250)|raw }}

    19 |

    Lieu : {{ drink.place }}

    20 |

    Nombre d'inscrits : {{ drink.participants_count }}

    21 |
    22 | 23 |
    24 | {{ drink.day|date("d") }} 25 | {{ drink.day|date("F")|trans }} 26 | {{ drink.hour|date("H:i") }} 27 |
    28 |
  • 29 | {% endfor %} 30 |
31 | {% else %} 32 |

Les développeurs PHP ont soif, ce serait bien d'organiser un apéro !

33 | {% endif %} 34 |
35 |
36 |
37 | 38 |
39 |
40 |
41 | 42 |

Les apéros PHP

43 |
44 |
45 |

Les apéroPHPs sont ouverts à tous (de débutants à confirmés), venez discuter du PHP mais pas que de PHP !

46 |

Boissons alcoolisées ou non, repas ou simple apéro, c'est à vous de le définir...

47 | 48 |
49 |
50 |
51 | 52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /src/Aperophp/Lib/AutoLinkTwigExtension.php: -------------------------------------------------------------------------------- 1 | '.$truncate_len.') 26 | { 27 | return $matches[1].\'\'.substr($matches[2].$matches[3], 0, '.$truncate_len.').\''.$pad.'\'.$matches[4]; 28 | } 29 | '; 30 | } 31 | 32 | $callback_function .= ' 33 | else 34 | { 35 | return $matches[1].\'\'.$matches[2].$matches[3].\'\'.$matches[4]; 36 | } 37 | '; 38 | 39 | 40 | $autoLinkRe = '~ 41 | ( # leading text 42 | <\w+.*?>| # leading HTML tag, or 43 | [^=!:\'"/]| # leading punctuation, or 44 | ^ # beginning of line 45 | ) 46 | ( 47 | (?:https?://)| # protocol spec, or 48 | (?:www\.) # www.* 49 | ) 50 | ( 51 | [-\w]+ # subdomain or domain 52 | (?:\.[-\w]+)* # remaining subdomains or domain 53 | (?::\d+)? # port 54 | (?:/(?:(?:[\~\w\+%-]|(?:[,.;:][^\s$]))+)?)* # path 55 | (?:\?[\w\+%&=.;-]+)? # query string 56 | (?:\#[\w\-/\?!=]*)? # trailing anchor 57 | ) 58 | ([[:punct:]]|\s|<|$) # trailing text 59 | ~x'; 60 | 61 | return preg_replace_callback( 62 | $autoLinkRe, 63 | create_function('$matches', $callback_function), 64 | $text 65 | ); 66 | 67 | 68 | }); 69 | return array( 70 | 'autolink' => $filter, 71 | ); 72 | } 73 | 74 | public function getName() 75 | { 76 | return "autolink"; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/Resources/views/common/topbar.html.twig: -------------------------------------------------------------------------------- 1 | 11 | 12 | 52 | -------------------------------------------------------------------------------- /app/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | query(file_get_contents($file)); 19 | }; 20 | 21 | $loadFixtures = function() use ($app, $importFile) { 22 | $importFile(__DIR__.'/../data/sql/fixtures.sql'); 23 | }; 24 | 25 | $console = new Application('Aperophp', '1'); 26 | 27 | $console->register('db:install') 28 | ->setDefinition(array( 29 | new InputOption('test', '', InputOption::VALUE_NONE, 'Test mode'), 30 | new InputOption('load-fixtures', '', InputOption::VALUE_NONE, 'Test mode'), 31 | )) 32 | ->setDescription('Create database') 33 | ->setHelp('Usage: php app/console db:install [--test] [--load-fixtures]') 34 | ->setCode( 35 | function(InputInterface $input, OutputInterface $output) use ($app, $goToTestEnv, $importFile, $loadFixtures) { 36 | if ($input->getOption('test')) { 37 | $goToTestEnv(); 38 | } 39 | 40 | $output->writeln('Create schema.'); 41 | $importFile(__DIR__ . '/../data/sql/schema.mysql.sql'); 42 | 43 | if ($input->getOption('load-fixtures')) { 44 | $output->writeln('Load fixtures.'); 45 | $loadFixtures(); 46 | } 47 | 48 | $output->writeln('Installation done.'); 49 | } 50 | ); 51 | 52 | $console->register('db:load-fixtures') 53 | ->setDefinition(array( 54 | new InputOption('test', '', InputOption::VALUE_NONE, 'Test mode'), 55 | )) 56 | ->setDescription('Create database') 57 | ->setHelp('Usage: php app/console db:load-fixtures [--test]') 58 | ->setCode( 59 | function(InputInterface $input, OutputInterface $output) use ($app, $goToTestEnv, $loadFixtures) { 60 | if ($input->getOption('test')) { 61 | $goToTestEnv(); 62 | } 63 | 64 | $loadFixtures(); 65 | } 66 | ); 67 | 68 | $console->add(new \Aperophp\Command\SyncWithMeetup($app['meetup_client'], $app['drinks'], $app['cities'], $app['users'], $app['drink_participants'], $app['meetup.groupurlnames'])); 69 | 70 | $console->run(); 71 | -------------------------------------------------------------------------------- /src/Aperophp/Form/Type/EditMemberType.php: -------------------------------------------------------------------------------- 1 | 15 | * @since 4 févr. 2012 16 | * @version 1.0 - 4 févr. 2012 - Koin 17 | */ 18 | class EditMemberType extends AbstractType 19 | { 20 | public function buildForm(FormBuilderInterface $builder, array $options) 21 | { 22 | $builder->addEventSubscriber(new DataFilterSubscriber($builder)); 23 | 24 | $builder->add( 25 | $builder->create('member', 'form') 26 | ->add('password', 'password', array( 27 | 'label' => 'Mot de passe', 28 | 'required' => false 29 | )) 30 | )->add( 31 | $builder->create('user', 'form') 32 | ->add('lastname', 'text', array( 33 | 'label' => 'Nom', 34 | 'required' => false, 35 | 'attr' => array( 36 | 'placeholder' => 'Facultatif.' 37 | ) 38 | )) 39 | ->add('firstname', 'text', array( 40 | 'label' => 'Prénom', 41 | 'required' => false, 42 | 'attr' => array( 43 | 'placeholder' => 'Facultatif.' 44 | ) 45 | )) 46 | ->add('email', 'email') 47 | ); 48 | } 49 | 50 | public function setDefaultOptions(OptionsResolverInterface $resolver) 51 | { 52 | $collectionConstraint = new Constraints\Collection(array( 53 | 'fields' => array( 54 | 'user' => new Constraints\Collection(array( 55 | 'fields' => array( 56 | 'lastname' => new Constraints\Length(array('max' => 80)), 57 | 'firstname' => new Constraints\Length(array('max' => 80)), 58 | 'email' => array( 59 | new Constraints\Email(), 60 | new Constraints\NotBlank(), 61 | ), 62 | ) 63 | )), 64 | 'member' => new Constraints\Collection(array( 65 | 'fields' => array( 66 | 'password' => new Constraints\Length(array('max' => 80)), 67 | ) 68 | )) 69 | ), 70 | )); 71 | 72 | $resolver->setDefaults(array( 73 | 'validation_constraint' => $collectionConstraint 74 | )); 75 | } 76 | 77 | public function getName() 78 | { 79 | return 'member_edit'; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/units_db/Aperophp/Repository/User.php: -------------------------------------------------------------------------------- 1 | assert 14 | ->if($user = $this->app['users']->findOneByMemberId(1)) 15 | ->then 16 | ->boolean(is_array($user))->isTrue() 17 | ; 18 | } 19 | 20 | public function testFindOneByUserId_withInexistingEntry_returnEmptyArray() 21 | { 22 | $this->assert 23 | ->if($user = $this->app['users']->findOneByMemberId(1111)) 24 | ->then 25 | ->boolean($user)->isFalse() 26 | ; 27 | } 28 | 29 | public function testFindOneByEmailToken_withExistingEntry_returnIt() 30 | { 31 | $this->assert 32 | ->if($user = $this->app['users']->findOneByEmailToken('user1@example.org', 'token')) 33 | ->then 34 | ->boolean(is_array($user))->isTrue() 35 | ; 36 | } 37 | 38 | public function testFindOneByEmailToken_withInexistingEntry_returnIt() 39 | { 40 | $this->assert 41 | ->if($user = $this->app['users']->findOneByEmailToken('user1@example.org', 'wrong-token')) 42 | ->then 43 | ->boolean($user)->isFalse() 44 | ; 45 | } 46 | 47 | public function testUpdate() 48 | { 49 | $this->assert 50 | ->if($user = $this->app['users']->find(1)) 51 | ->then 52 | ->boolean(is_array($user))->isTrue() 53 | ->string($user['firstname'])->isEqualTo('User1') 54 | ->string($user['lastname'])->isEqualTo('Example1') 55 | ; 56 | 57 | $user['firstname'] = 'foo'; 58 | $user['lastname'] = 'bar'; 59 | 60 | $this->assert 61 | ->integer($this->app['users']->update($user, array('id' => 1)))->isEqualTo(1) 62 | ; 63 | 64 | $this->assert 65 | ->if($user = $this->app['users']->find(1)) 66 | ->then 67 | ->boolean(is_array($user))->isTrue() 68 | ->string($user['firstname'])->isEqualTo('foo') 69 | ->string($user['lastname'])->isEqualTo('bar') 70 | ; 71 | } 72 | 73 | public function testFindOneByEmail_withExistingEntry_returnIt() 74 | { 75 | $this->assert 76 | ->if($user = $this->app['users']->findOneByEmail('user1@example.org')) 77 | ->then 78 | ->boolean(is_array($user))->isTrue() 79 | ; 80 | } 81 | 82 | public function testFindOneByEmail_withInexistingEntry_returnIt() 83 | { 84 | $this->assert 85 | ->if($user = $this->app['users']->findOneByEmail('user42@example.org')) 86 | ->then 87 | ->boolean($user)->isFalse() 88 | ; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Aperophp/Form/Type/DrinkCommentType.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 18 févr. 2012 17 | * @version 1.0 - 18 févr. 2012 - Koin 18 | */ 19 | class DrinkCommentType extends AbstractType 20 | { 21 | protected $session; 22 | 23 | public function __construct(SessionInterface $session) 24 | { 25 | $this->session = $session; 26 | } 27 | 28 | public function buildForm(FormBuilderInterface $builder, array $options) 29 | { 30 | $builder->addEventSubscriber(new DataFilterSubscriber($builder)); 31 | 32 | if (!$this->session->has('member')) { 33 | $builder->add( 34 | $builder->create('user', 'form') 35 | ->add('lastname', 'text', array( 36 | 'label' => 'Nom', 37 | 'required' => false, 38 | 'attr' => array( 39 | 'placeholder' => 'Facultatif.' 40 | ) 41 | )) 42 | ->add('firstname', 'text', array( 43 | 'label' => 'Prénom', 44 | 'required' => true 45 | )) 46 | ->add('email', 'email') 47 | ); 48 | } 49 | 50 | $builder 51 | ->add('content', 'textarea', array( 52 | 'label' => 'Commentaire' 53 | )) 54 | ->add('captcha', 'hidden') 55 | ; 56 | } 57 | 58 | public function setDefaultOptions(OptionsResolverInterface $resolver) 59 | { 60 | $fields = array( 61 | 'captcha' => new Constraints\Blank(array('message' => "Vous avez été détecté comme un robot.")), 62 | 'content' => new Constraints\NotNull(), 63 | ); 64 | 65 | if (!$this->session->has('member')) { 66 | $fields['user'] = new Constraints\Collection(array( 67 | 'lastname' => new Constraints\Length(array('max' => 80)), 68 | 'firstname' => new Constraints\Length(array('max' => 80)), 69 | 'email' => array( 70 | new Constraints\Email(), 71 | new Constraints\NotNull(), 72 | ) 73 | )); 74 | } 75 | $collectionConstraint = new Constraints\Collection(array( 76 | 'fields' => $fields 77 | )); 78 | 79 | $resolver->setDefaults(array( 80 | 'validation_constraint' => $collectionConstraint, 81 | )); 82 | } 83 | 84 | public function getName() 85 | { 86 | return 'drink_comment'; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /bin/assets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Localisation 4 | # 5 | SCRIPTDIR=$(cd $(dirname "$0"); pwd) 6 | MAINDIR=$(cd $(dirname "$DIR"); pwd) 7 | BOOTSTRAPDIR=$MAINDIR/vendor/twbs/bootstrap 8 | [ -d $MAINDIR/tmp ] || mkdir $MAINDIR/tmp 9 | DATE=$(date +%H:%M%p) 10 | CHECK="\\033[1;32m✔\\033[0;39m" 11 | HR=\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\#\# 12 | 13 | echo "${HR}" 14 | echo "Building Bootstrap..." 15 | echo "${HR}" 16 | 17 | # 18 | # Intégration FontAwesome 19 | # 20 | cp $MAINDIR/vendor/FortAwesome/Font-Awesome/font/font* $MAINDIR/web/font/ 21 | cp $MAINDIR/vendor/FortAwesome/Font-Awesome/css/font-awesome-ie7.css $MAINDIR/web/css/ 22 | echo -e " - Integrate FontAwesome... ${CHECK} Done" 23 | 24 | # 25 | # Compilation Twitter Bootstrap 26 | # 27 | cp $BOOTSTRAPDIR/less/bootstrap.less $MAINDIR/tmp/ 28 | cp $BOOTSTRAPDIR/less/responsive.less $MAINDIR/tmp/ 29 | sed -i '' -e "s!import \"!import \"../vendor/twbs/bootstrap/less/!" -e 's!vendor/twbs/bootstrap/less/sprites.less";!vendor/FortAwesome/Font-Awesome/less/font-awesome.less";!' $MAINDIR/tmp/bootstrap.less 30 | echo -e "@import \"../assets/apero.less\";" >> $MAINDIR/tmp/bootstrap.less 31 | sed -i '' -e "s!import \"!import \"../vendor/twbs/bootstrap/less/!" $MAINDIR/tmp/responsive.less 32 | echo -e "@import \"../assets/apero-responsive.less\";" >> $MAINDIR/tmp/responsive.less 33 | echo -e " - Prepare Bootstrap less files... ${CHECK} Done" 34 | jshint $BOOTSTRAPDIR/js/*.js --config $BOOTSTRAPDIR/js/.jshintrc 35 | echo -e " - Running JSHint on javascript... ${CHECK} Done" 36 | recess --compile $MAINDIR/tmp/bootstrap.less > $MAINDIR/web/css/bootstrap.css 37 | recess --compress $MAINDIR/tmp/bootstrap.less > $MAINDIR/web/css/bootstrap.min.css 38 | recess --compile $MAINDIR/tmp/responsive.less > $MAINDIR/web/css/bootstrap-responsive.css 39 | recess --compress $MAINDIR/tmp/responsive.less > $MAINDIR/web/css/bootstrap-responsive.min.css 40 | echo -e " - Compiling LESS with Recess... ${CHECK} Done" 41 | cat $BOOTSTRAPDIR/js/bootstrap-transition.js $BOOTSTRAPDIR/js/bootstrap-alert.js $BOOTSTRAPDIR/js/bootstrap-button.js $BOOTSTRAPDIR/js/bootstrap-carousel.js $BOOTSTRAPDIR/js/bootstrap-collapse.js $BOOTSTRAPDIR/js/bootstrap-dropdown.js $BOOTSTRAPDIR/js/bootstrap-modal.js $BOOTSTRAPDIR/js/bootstrap-tooltip.js $BOOTSTRAPDIR/js/bootstrap-popover.js $BOOTSTRAPDIR/js/bootstrap-scrollspy.js $BOOTSTRAPDIR/js/bootstrap-tab.js $BOOTSTRAPDIR/js/bootstrap-typeahead.js > $MAINDIR/tmp/bootstrap.js 42 | uglifyjs $MAINDIR/tmp/bootstrap.js > $MAINDIR/tmp/bootstrap.min.tmp.js 43 | echo -e "/*!\n* Bootstrap.js by @fat & @mdo\n* Copyright 2012 Twitter, Inc.\n* http://www.apache.org/licenses/LICENSE-2.0.txt\n*/" > $MAINDIR/tmp/copyright.js 44 | cat $MAINDIR/tmp/copyright.js $MAINDIR/tmp/bootstrap.min.tmp.js > $MAINDIR/web/js/bootstrap.min.js 45 | echo -e " - Compiling and minifying javascript... ${CHECK} Done" 46 | rm -Rf $MAINDIR/tmp 47 | echo -e " - Cleaning... ${CHECK} Done" 48 | 49 | echo "${HR}" 50 | echo "Bootstrap for AperoPHP successfully built at ${DATE}" 51 | echo "${HR}" 52 | -------------------------------------------------------------------------------- /src/Resources/views/drink/_participate.html.twig: -------------------------------------------------------------------------------- 1 | {% form_theme participationForm 'common/Form/fields.html.twig' %} 2 | 3 |
4 | 8 | 49 | 53 |
54 | -------------------------------------------------------------------------------- /src/Resources/views/layout.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Apéro PHP 6 | 7 | {% block metas %}{% endblock %} 8 | 9 | 10 | 13 | 14 | {% block headjs %}{% endblock %} 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% block headcss %}{% endblock %} 32 | 33 | 34 | 35 | {% include 'common/ga.html.twig' %} 36 | 37 | 38 | 39 | {% include 'common/topbar.html.twig' %} 40 |
41 |
42 |
43 |
44 |
45 | {# ******* 46 | ** Display flash 47 | ******* #} 48 | {% if app.session.flashbag.has('success') %}
{{ app.session.flashbag.get('success')|join('
') }}
{% endif %} 49 | {% if app.session.flashbag.has('notice') %}
{{ app.session.flashbag.get('notice')|join('
') }}
{% endif %} 50 | {% if app.session.flashbag.has('error') %}
{{ app.session.flashbag.get('error')|join('
') }}
{% endif %} 51 | {# ******* #} 52 |
53 | {% block content %}{% endblock %} 54 |
55 |
56 | {% include 'common/footer.html.twig' %} 57 |
58 |
59 |
60 | 61 | 62 | 63 | {% block bottomjs %}{% endblock %} 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/Aperophp/Form/Type/SignupType.php: -------------------------------------------------------------------------------- 1 | 15 | * @since 22 janv. 2012 16 | * @version 1.0 - 22 janv. 2012 - Koin 17 | */ 18 | class SignupType extends AbstractType 19 | { 20 | public function buildForm(FormBuilderInterface $builder, array $options) 21 | { 22 | $builder->addEventSubscriber(new DataFilterSubscriber($builder)); 23 | 24 | $builder->add( 25 | $builder->create('member', 'form') 26 | ->add('username', 'text', array( 27 | 'label' => 'Identifiant' 28 | )) 29 | ->add('password', 'password', array( 30 | 'label' => 'Mot de passe' 31 | )) 32 | )->add( 33 | $builder->create('user', 'form') 34 | ->add('lastname', 'text', array( 35 | 'label' => 'Nom', 36 | 'required' => false, 37 | 'attr' => array( 38 | 'placeholder' => 'Facultatif.' 39 | ) 40 | )) 41 | ->add('firstname', 'text', array( 42 | 'label' => 'Prénom', 43 | 'required' => false, 44 | 'attr' => array( 45 | 'placeholder' => 'Facultatif.' 46 | ) 47 | )) 48 | ->add('email', 'email') 49 | ); 50 | } 51 | 52 | public function setDefaultOptions(OptionsResolverInterface $resolver) 53 | { 54 | $collectionConstraint = new Constraints\Collection(array( 55 | 'fields' => array( 56 | 'user' => new Constraints\Collection(array( 57 | 'fields' => array( 58 | 'lastname' => new Constraints\Length(array('max' => 80)), 59 | 'firstname' => new Constraints\Length(array('max' => 80)), 60 | 'email' => array( 61 | new Constraints\NotBlank(), 62 | new Constraints\Email(), 63 | ), 64 | ), 65 | )), 66 | 'member' => new Constraints\Collection(array( 67 | 'fields' => array( 68 | 'username' => array( 69 | new Constraints\NotBlank(), 70 | new Constraints\Length(array('max' => 80)), 71 | ), 72 | 'password' => array( 73 | new Constraints\NotBlank(), 74 | new Constraints\Length(array('min' => 4, 'max' => 80)), 75 | ), 76 | ) 77 | )) 78 | ), 79 | )); 80 | 81 | $resolver->setDefaults(array( 82 | 'validation_constraint' => $collectionConstraint 83 | )); 84 | } 85 | 86 | public function getName() 87 | { 88 | return 'signup'; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Resources/views/member/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 | 8 |

Mon profil

9 |
10 |
11 |
12 |
13 |
14 | 15 |
16 | {{ form_widget(form.user.lastname) }} 17 | {{ form_errors(form.user.lastname) }} 18 |
19 |
20 |
21 | 22 |
23 | {{ form_widget(form.user.firstname) }} 24 | {{ form_errors(form.user.firstname) }} 25 |
26 |
27 |
28 | 29 |
30 | 31 |
32 |
33 |
34 | 35 |
36 |
37 | 38 | {{ form_widget(form.user.email, { 'attr': {'class': 'span2'} }) }} 39 | {{ form_errors(form.user.email) }} 40 |
41 |
42 |
43 |
44 | 45 |
46 | {{ form_widget(form.member.password) }} 47 | {{ form_errors(form.member.password) }} 48 |
49 |
50 | {{ form_rest(form) }} 51 |
52 | 53 | 54 |
55 |
56 |
57 |
58 |
59 |
60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /src/Aperophp/Repository/Repository.php: -------------------------------------------------------------------------------- 1 | db = $db; 28 | } 29 | 30 | /** 31 | * Inserts a table row with specified data. 32 | * 33 | * @param array $data An associative array containing column-value pairs. 34 | * 35 | * @return integer The number of affected rows. 36 | */ 37 | public function insert(array $data) 38 | { 39 | return $this->db->insert($this->getTableName(), $data); 40 | } 41 | 42 | /** 43 | * Executes an SQL UPDATE statement on a table. 44 | * 45 | * @param array $data An associative array containing column-value pairs. 46 | * @param array $identifier The update criteria 47 | * 48 | * @return integer The number of affected rows. 49 | */ 50 | public function update(array $data, array $identifier) 51 | { 52 | return $this->db->update($this->getTableName(), $data, $identifier); 53 | } 54 | 55 | /** 56 | * Executes an SQL DELETE statement on a table. 57 | * 58 | * @param array $identifier The deletion criteria. An associateve array containing column-value pairs. 59 | * 60 | * @return integer The number of affected rows. 61 | */ 62 | public function delete(array $identifier) 63 | { 64 | return $this->db->delete($this->getTableName(), $identifier); 65 | } 66 | 67 | /** 68 | * Returns a record by supplied id 69 | * 70 | * @param mixed $id 71 | * 72 | * @return array 73 | */ 74 | public function find($id) 75 | { 76 | return $this->findByAttr('id', (int)$id); 77 | } 78 | 79 | /** 80 | * @param string $meetupId 81 | * 82 | * @return array 83 | */ 84 | public function findByMeetupId($meetupId) 85 | { 86 | return $this->findByAttr('meetup_com_id', $meetupId); 87 | } 88 | 89 | /** 90 | * @param string $attr 91 | * @param string $value 92 | * 93 | * @return array 94 | */ 95 | protected function findByAttr($attr, $value) 96 | { 97 | return $this->db->fetchAssoc(sprintf('SELECT * FROM %s WHERE %s = ? LIMIT 1', $this->getTableName(), $attr), array($value)); 98 | } 99 | 100 | /** 101 | * Returns all records from this repository's table 102 | * 103 | * @param integer $limit 104 | * 105 | * @return array 106 | */ 107 | public function findAll($limit = null) 108 | { 109 | if (null === $limit) { 110 | return $this->db->fetchAll(sprintf('SELECT * FROM %s', $this->getTableName())); 111 | } 112 | 113 | return $this->db->fetchAll(sprintf('SELECT * FROM %s LIMIT %d', $this->getTableName(), $limit)); 114 | } 115 | 116 | /** 117 | * Returns the last inserted id 118 | * 119 | * @return integer 120 | */ 121 | public function lastInsertId() 122 | { 123 | return $this->db->lastInsertId(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "afup/aperophp", 3 | "description" : "Source code for aperophp.net website", 4 | "license" : "GPL-3.0+", 5 | "minimum-stability" : "dev", 6 | "require": { 7 | "silex/silex" : "1.0.*", 8 | "doctrine/common" : "2.4.*", 9 | "doctrine/dbal" : "2.4.*", 10 | "twig/twig" : "1.12.*", 11 | "twig/extensions" : "dev-master", 12 | "symfony/class-loader" : "2.1.x", 13 | "symfony/form" : "2.1.x", 14 | "symfony/validator" : "2.1.x", 15 | "symfony/translation" : "2.1.x", 16 | "symfony/config" : "2.1.x", 17 | "symfony/browser-kit" : "2.1.x", 18 | "symfony/console" : "2.1.x", 19 | "symfony/css-selector" : "2.1.x", 20 | "symfony/twig-bridge" : "2.1.x", 21 | "swiftmailer/swiftmailer" : "4.2.*", 22 | "twbs/bootstrap" : "2.0.4", 23 | "FortAwesome/Font-Awesome" : "~3.2", 24 | "mheap/Silex-Gravatar" : "master", 25 | "Gravatar/Gravatar" : "master", 26 | "michelf/php-markdown" : "1.3.x-dev", 27 | "tijsverkoyen/akismet" : "1.1.0", 28 | "dms/meetup-api-client": "^2.0", 29 | "atoum/stubs": "^2.5" 30 | }, 31 | "require-dev": { 32 | "atoum/atoum" : "dev-master" 33 | }, 34 | "autoload": { 35 | "psr-0": { 36 | "Aperophp": "src/", 37 | "Gravatar": "vendor/Gravatar/Gravatar/src/" 38 | }, 39 | "classmap": ["vendor/mheap/Silex-Gravatar/src/SilexGravatar/GravatarExtension.php"] 40 | }, 41 | "repositories": [ 42 | { 43 | "type": "package", 44 | "package": { 45 | "version": "2.0.4", 46 | "name": "twbs/bootstrap", 47 | "source": { 48 | "url": "https://github.com/twbs/bootstrap.git", 49 | "type": "git", 50 | "reference": "master" 51 | }, 52 | "dist": { 53 | "url": "https://github.com/twbs/bootstrap/zipball/v2.0.4", 54 | "type": "zip" 55 | } 56 | } 57 | }, 58 | { 59 | "type": "package", 60 | "package": { 61 | "version": "master", 62 | "name": "mheap/Silex-Gravatar", 63 | "source": { 64 | "url": "https://github.com/mheap/Silex-Gravatar.git", 65 | "type": "git", 66 | "reference": "master" 67 | }, 68 | "dist": { 69 | "url": "https://github.com/mheap/Silex-Gravatar/zipball/master", 70 | "type": "zip" 71 | } 72 | } 73 | }, 74 | { 75 | "type": "package", 76 | "package": { 77 | "name": "Gravatar/Gravatar", 78 | "version": "master", 79 | "source": { 80 | "url": "https://github.com/sveneisenschmidt/Gravatar-php.git", 81 | "type": "git", 82 | "reference": "master" 83 | }, 84 | "dist": { 85 | "url": "https://github.com/sveneisenschmidt/Gravatar-php/zipball/master", 86 | "type": "zip" 87 | } 88 | } 89 | } 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /tests/units_db/Aperophp/Provider/Controller/Comment.php: -------------------------------------------------------------------------------- 1 | '', 16 | 'drink_comment[user][firstname]' => 'Foo', 17 | 'drink_comment[user][lastname]' => 'Bar', 18 | 'drink_comment[user][email]' => 'foobar@example.org', 19 | 'drink_comment[content]' => 'Super apéro.', 20 | ), 21 | $data_overload 22 | ); 23 | } 24 | 25 | public function testCommentDrinkWithUnanonymousUser() 26 | { 27 | $this->assert 28 | ->if($client = $this->createClient()) 29 | ->and($crawler = $client->request('GET', '/1/view.html')) 30 | ->then() 31 | ->boolean($client->getResponse()->isOk())->isTrue() 32 | ->integer($crawler->filter('blockquote')->count()-1)->isEqualTo(2) 33 | ->if($form = $crawler->selectButton('comment')->form()) 34 | ->and($crawler = $client->submit($form, $this->getDefaultDatas())) 35 | ->then() 36 | ->boolean($client->getResponse()->isRedirect('/1/view.html'))->isTrue() 37 | ->if($crawler = $client->followRedirect()) 38 | ->then() 39 | ->boolean($client->getResponse()->isOk())->isTrue() 40 | ->integer($crawler->filter('div.alert-success')->count())->isEqualTo(1) 41 | ->integer($crawler->filter('blockquote')->count()-1)->isEqualTo(3) 42 | ; 43 | } 44 | 45 | public function testNewCommentDrink_withNoCaptcha_isNotCreated() 46 | { 47 | $this->assert 48 | ->if($client = $this->createClient()) 49 | ->and($crawler = $client->request('GET', '/1/view.html')) 50 | ->then() 51 | ->boolean($client->getResponse()->isOk())->isTrue() 52 | ->integer($crawler->filter('blockquote')->count()-1)->isEqualTo(2) 53 | ->if($form = $crawler->selectButton('comment')->form()) 54 | ->and($crawler = $client->submit($form, $this->getDefaultDatas(array('drink_comment[user][firstname]' => 'DO NOT FEED')))) 55 | ->then() 56 | ->boolean($client->getResponse()->isRedirect('/drink/1/view.html'))->isFalse() 57 | ; 58 | } 59 | 60 | public function testCommentDrinkWithNoData() 61 | { 62 | $this->assert 63 | ->if($client = $this->createClient()) 64 | ->and($crawler = $client->request('GET', '/1/view.html')) 65 | ->then() 66 | ->boolean($client->getResponse()->isOk())->isTrue() 67 | ->integer($crawler->filter('blockquote')->count()-1)->isEqualTo(2) 68 | ->if($form = $crawler->selectButton('comment')->form()) 69 | ->and($crawler = $client->submit($form, $this->getDefaultDatas(array( 70 | 'drink_comment[user][firstname]' => '', 71 | 'drink_comment[user][lastname]' => '', 72 | 'drink_comment[user][email]' => '', 73 | 'drink_comment[content]' => '', 74 | )))) 75 | ->then() 76 | ->boolean($client->getResponse()->isRedirect('/drink/1/view.html'))->isFalse() 77 | ; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Aperophp/Repository/Drink.php: -------------------------------------------------------------------------------- 1 | db->fetchAll($sql); 43 | } 44 | 45 | 46 | /** 47 | * Find futur drinks order by day, with participants 48 | */ 49 | public function findNext($limit = null) 50 | { 51 | if (null === $limit) { 52 | $limit = 3; 53 | } 54 | 55 | $today = new \DateTime(); 56 | 57 | $sql = sprintf( 58 | 'SELECT d.*, m.username as organizer_username, u.email as organizer_email, c.name as city_name, 59 | (%s) as participants_count 60 | FROM Drink d 61 | JOIN City c ON (d.city_id = c.id) 62 | LEFT JOIN Member m ON (d.member_id = m.id) 63 | LEFT JOIN User u ON (m.id = u.member_id) 64 | WHERE d.day >= "%s" 65 | ORDER BY day ASC 66 | LIMIT %s 67 | ', 68 | self::getCountParticipantsQuery(), 69 | $today->format('Y-m-d') , 70 | $limit); 71 | 72 | return $this->db->fetchAll($sql); 73 | } 74 | 75 | /** 76 | * @param int $id 77 | * 78 | * @return array 79 | */ 80 | public function find($id) 81 | { 82 | return $this->findByAttr('id', (int) $id); 83 | } 84 | 85 | /** 86 | * @param int $meetupId 87 | * 88 | * @return array 89 | */ 90 | public function findByMeetupId($meetupId) 91 | { 92 | return $this->findByAttr('meetup_com_id', $meetupId); 93 | } 94 | 95 | /** 96 | * Load a specific drink 97 | * 98 | * @param string $attr 99 | * @param int $value 100 | * 101 | * @return array 102 | */ 103 | protected function findByAttr($attr, $value) 104 | { 105 | $sql = 106 | sprintf('SELECT d.*, m.username as organizer_username, u.email as organizer_email, c.name as city_name, 107 | (%s) as participants_count, m.id as member_id 108 | FROM Drink d 109 | LEFT JOIN Member m ON (d.member_id = m.id) 110 | LEFT JOIN User u ON (u.member_id = m.id) 111 | JOIN City c ON (d.city_id = c.id) 112 | WHERE d.%s = ? 113 | LIMIT 1 114 | ', self::getCountParticipantsQuery(), $attr); 115 | 116 | return $this->db->fetchAssoc($sql, array($value)); 117 | } 118 | 119 | public function findAllKindsInAssociativeArray() 120 | { 121 | return array( 122 | self::KIND_DRINK => self::KIND_DRINK, 123 | self::KIND_CONFERENCE => self::KIND_CONFERENCE, 124 | ); 125 | } 126 | 127 | public static function getCountParticipantsQuery() 128 | { 129 | return "SELECT COUNT(*) FROM Drink_Participation WHERE drink_id = d.id AND percentage > 0"; 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /tests/units_db/Aperophp/Repository/Drink.php: -------------------------------------------------------------------------------- 1 | assert 14 | ->if($drinks = $this->app['drinks']->findAll()) 15 | ->then 16 | ->boolean(is_array($drinks))->isTrue() 17 | ->integer(count($drinks))->isEqualTo(2) 18 | ; 19 | 20 | foreach ($drinks as $drink) { 21 | $this->assert 22 | ->boolean(is_array($drink))->isTrue() 23 | ->boolean(array_key_exists('participants_count', $drink))->isTrue() 24 | ->boolean(array_key_exists('organizer_username', $drink))->isTrue() 25 | ->boolean(array_key_exists('organizer_email', $drink))->isTrue() 26 | ->boolean(array_key_exists('city_name', $drink))->isTrue() 27 | ; 28 | } 29 | } 30 | 31 | public function testFindAll() 32 | { 33 | $this->assert 34 | ->if($drinks = $this->app['drinks']->findAll(3)) 35 | ->then 36 | ->boolean(is_array($drinks))->isTrue() 37 | ->integer(count($drinks))->isEqualTo(2) 38 | ; 39 | 40 | foreach ($drinks as $drink) { 41 | $this->assert 42 | ->boolean(is_array($drink))->isTrue() 43 | ->boolean(array_key_exists('participants_count', $drink))->isTrue() 44 | ->boolean(array_key_exists('organizer_username', $drink))->isTrue() 45 | ->boolean(array_key_exists('organizer_email', $drink))->isTrue() 46 | ->boolean(array_key_exists('city_name', $drink))->isTrue() 47 | ; 48 | } 49 | } 50 | 51 | public function testFindNext() 52 | { 53 | $this->assert 54 | ->if($drinks = $this->app['drinks']->findNext()) 55 | ->then 56 | ->boolean(is_array($drinks))->isTrue() 57 | ->integer(count($drinks))->isEqualTo(1) 58 | ; 59 | 60 | foreach ($drinks as $drink) { 61 | $this->assert 62 | ->boolean(is_array($drink))->isTrue() 63 | ->boolean(array_key_exists('participants_count', $drink))->isTrue() 64 | ->boolean(array_key_exists('organizer_username', $drink))->isTrue() 65 | ->boolean(array_key_exists('organizer_email', $drink))->isTrue() 66 | ->boolean(array_key_exists('city_name', $drink))->isTrue() 67 | ; 68 | } 69 | } 70 | 71 | public function testFind_withExistingDrink_returnArray() 72 | { 73 | $this->assert 74 | ->if($drink = $this->app['drinks']->find(1)) 75 | ->then 76 | ->boolean(is_array($drink))->isTrue() 77 | ->boolean(is_array($drink))->isTrue() 78 | ->boolean(array_key_exists('participants_count', $drink))->isTrue() 79 | ->boolean(array_key_exists('organizer_username', $drink))->isTrue() 80 | ->boolean(array_key_exists('organizer_email', $drink))->isTrue() 81 | ->boolean(array_key_exists('city_name', $drink))->isTrue() 82 | ; 83 | } 84 | 85 | public function testFind_withInexistingDrink_returnFalse() 86 | { 87 | $this->assert 88 | ->if($drink = $this->app['drinks']->find(13984)) 89 | ->then 90 | ->boolean($drink)->isFalse() 91 | ; 92 | } 93 | 94 | public function testFindAllKindsInAssociativeArray() 95 | { 96 | $this->assert 97 | ->if($drinks = $this->app['drinks']->findAllKindsInAssociativeArray()) 98 | ->then 99 | ->boolean(is_array($drinks))->isTrue() 100 | ->integer(count($drinks))->isEqualTo(2) 101 | ; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AperoPHP 2 | ======== 3 | 4 | [![Build Status](https://secure.travis-ci.org/afup/aperophp.png?branch=master)](http://travis-ci.org/afup/aperophp) 5 | 6 | Install 7 | ======= 8 | 9 | Requirements 10 | ------------ 11 | 12 | * PHP 5.4 13 | * MySQL 14 | * Apache 15 | 16 | Project have been builded and his production environment OS is a Debian Squeeze 17 | 18 | Minimal installation 19 | -------------------- 20 | 21 | * Clone project 22 | * Install configuration 23 | * Copy app/config.php.dist to config.php 24 | * Update configuration for your installation (typically, database connexion) 25 | * Install dependancies 26 | ```bash 27 | cd /path/to/project 28 | php composer.phar install 29 | ``` 30 | * Install and populate database 31 | ```bash 32 | app/console db:install 33 | app/console db:load-fixtures 34 | ``` 35 | 36 | To generate assets 37 | ------------------ 38 | 39 | In order to generate assets, you have to download npm [here](http://npmjs.org/ "npm official website"). 40 | Then, run the following command: 41 | 42 | npm install 43 | 44 | You can now generate assets with: 45 | 46 | ./bin/assets.sh 47 | 48 | Vhost example 49 | ------------- 50 | 51 | 52 | DocumentRoot "/path/to/" 53 | ServerName www.aperophp.dev 54 | 55 | 56 | Options Indexes Includes FollowSymLinks -MultiViews 57 | AllowOverride All 58 | Order allow,deny 59 | Allow from all 60 | 61 | RewriteEngine On 62 | RewriteCond %{REQUEST_FILENAME} !-f 63 | RewriteRule ^(.*)$ index.php [QSA,L] 64 | 65 | 66 | 67 | Comment participer 68 | ================== 69 | 70 | [Comment participer ?](https://github.com/afup/aperophp/wiki/Comment-contribuer) 71 | 72 | Comment déployer 73 | ================ 74 | 75 | Le déploiement est assuré par [Capistrano](https://github.com/capistrano/capistrano) qui est un programme en Ruby. 76 | 77 | Pour déployer, il vous faudra donc disposer d'un environnement Ruby fonctionnel ([exemple pour Mac OS X](http://pym.me/posts/installer-et-configurer-un-environnement-de-developpement-ruby-sur-mac-os-x/)). 78 | 79 | Pour installer Capistrano et les dépendances nécessaire, il faut disposer d'une installation fonctionnelle de [Bundler](http://bundler.io/) 80 | Une fois l'installation de Bundler fonctionnelle, il suffit de lancer la commande suivante : 81 | 82 | ``` 83 | bundle install 84 | ``` 85 | 86 | Pour déployer le projet, il suffit alors de taper la commande `bundle exec cap deploy` 87 | 88 | NB : il est nécessaire que votre clé SSH soit autorisée sur l'utilisateur pour pouvoir déployer 89 | 90 | Premier déploiement 91 | ------------------- 92 | 93 | Voici les étapes à faire attention pour un 1er déploiement 94 | 95 | * Récupérer les informations du serveurs (utilisateur SSH, hôte, path d'installation) et s'assurer qu'une clé SSH est autorisée sur ce couple 96 | * Modifier la configuration du serveur de déploiement (config/deploy.rb) 97 | * Initialiser le dossier de déploiement avec la commande `bundle exec cap deploy:setup` 98 | * Créer les fichiers partagés (shared/app/config.php) 99 | * Cloner manuellement le projet Git dans un dossier temporaire pour autoriser le host 100 | * Tester un déploiement `bundle exec cap deploy` 101 | 102 | TODO 103 | ==== 104 | 105 | Nice to have 106 | ------------ 107 | 108 | * authentification avec des services tierces (Openid, Twitter, Google, Facebook, etc.) oui 109 | * mise en avant des membres AFUP 110 | * mini-système de news pour le site 111 | * lien avec les antennes locales de l'AFUP (pour Lyon, Nantes, Orléans, par exemple) 112 | * accès et gestion directe depuis le back-office de l'AFUP 113 | 114 | To add as issues ? 115 | ------------------ 116 | 117 | * Après connexion, devrait être loggé (vérifier si check email => si oui, mettre un message) 118 | * Depot de commentaire : écran pas clair 119 | * Si dépot de 2 commentaires dans la même action, texte du 1er commentaire affiché au 2nd affichage 120 | -------------------------------------------------------------------------------- /src/Aperophp/Form/Type/DrinkParticipationType.php: -------------------------------------------------------------------------------- 1 | 17 | * @version 1.1 - 22 fev. 2012 - Gautier DI FOLCO 18 | */ 19 | class DrinkParticipationType extends AbstractType 20 | { 21 | protected $session; 22 | protected $drinkParticipantRepository; 23 | protected $presences = null; 24 | 25 | public function __construct(SessionInterface $session, Repository\DrinkParticipant $drinkParticipantRepository) 26 | { 27 | $this->session = $session; 28 | $this->drinkParticipantRepository = $drinkParticipantRepository; 29 | } 30 | 31 | public function buildForm(FormBuilderInterface $builder, array $options) 32 | { 33 | $builder->addEventSubscriber(new DataFilterSubscriber($builder)); 34 | 35 | if (!$this->session->has('member')) { 36 | $builder->add( 37 | $builder->create('user', 'form') 38 | ->add('lastname', 'text', array( 39 | 'label' => 'Nom', 40 | 'required' => false, 41 | 'attr' => array( 42 | 'placeholder' => 'Facultatif.' 43 | ) 44 | )) 45 | ->add('firstname', 'text', array( 46 | 'label' => 'Prénom', 47 | 'required' => true 48 | )) 49 | ->add('email', 'email') 50 | ); 51 | } 52 | 53 | $builder 54 | ->add('percentage', 'choice', array( 55 | 'label' => 'Participation', 56 | 'choices' => $options['presences'], 57 | 'attr' => array( 58 | 'size' => count($this->getPresences()) 59 | ) 60 | )) 61 | ->add('reminder', 'checkbox', array( 62 | 'label' => 'Me rappeler l\'évènement', 63 | 'required' => false 64 | )); 65 | } 66 | 67 | public function setDefaultOptions(OptionsResolverInterface $resolver) 68 | { 69 | $fields = array( 70 | 'percentage' => array( 71 | new Constraints\NotNull(), 72 | new Constraints\Choice(array( 73 | 'choices' => array_keys($this->getPresences()) 74 | )), 75 | ), 76 | 'reminder' => array(), 77 | ); 78 | 79 | if (!$this->session->has('member')) { 80 | $fields['user'] = new Constraints\Collection(array( 81 | 'lastname' => new Constraints\Length(array('max' => 80)), 82 | 'firstname' => new Constraints\Length(array('max' => 80)), 83 | 'email' => array( 84 | new Constraints\Email(), 85 | new Constraints\NotNull(), 86 | ) 87 | )); 88 | } 89 | $collectionConstraint = new Constraints\Collection(array( 90 | 'fields' => $fields 91 | )); 92 | 93 | $resolver->setDefaults(array( 94 | 'validation_constraint' => $collectionConstraint, 95 | 'presences' => $this->getPresences(), 96 | )); 97 | } 98 | 99 | public function getName() 100 | { 101 | return 'drink_participate'; 102 | } 103 | 104 | protected function getPresences() 105 | { 106 | if (null === $this->presences) { 107 | $this->presences = $this->drinkParticipantRepository->findAllPresencesInAssociativeArray(); 108 | } 109 | 110 | return $this->presences; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Resources/views/drink/new.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% form_theme form 'common/Form/fields.html.twig' %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 | 10 |

Organiser un apéro

11 |
12 |
13 |
14 | {{ form_errors(form) }} 15 |
16 |
17 | 18 |
19 | 20 |
21 |
22 |
23 | {{ form_label(form.hour, null, { 'label_attr': {'class': 'control-label'} }) }} 24 |
25 | {{ form_widget(form.hour) }} 26 | {{ form_errors(form.hour) }} 27 |
28 |
29 |
30 | {{ form_label(form.description, null, { 'label_attr': {'class': 'control-label'} }) }} 31 |
32 | {{ form_widget(form.description) }} 33 | {{ form_errors(form.description) }} 34 |
35 |
36 |
37 | {{ form_label(form.city_id, null, { 'label_attr': {'class': 'control-label'} }) }} 38 |
39 | {{ form_widget(form.city_id) }} 40 | {{ form_errors(form.city_id) }} 41 |
42 |
43 |
44 | {{ form_label(form.kind, null, { 'label_attr': {'class': 'control-label'} }) }} 45 |
46 | {{ form_widget(form.kind) }} 47 | {{ form_errors(form.kind) }} 48 |
49 |
50 |
51 | 52 |
53 |

54 |

55 |

56 |
57 |
58 | {{ form_rest(form) }} 59 |
60 | 61 | 62 |
63 |
64 |
65 |
66 |
67 |
68 | {% endblock %} 69 | 70 | {% block bottomjs %} 71 | 72 | 73 | 74 | 78 | {% endblock %} 79 | -------------------------------------------------------------------------------- /src/Resources/views/member/signup.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% form_theme form 'common/Form/fields.html.twig' %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 | 10 |

Inscription

11 |
12 |
13 |
14 |
15 |
16 | {{ form_label(form.user.lastname, null, { 'label_attr': {'class': 'control-label'} }) }} 17 |
18 | {{ form_widget(form.user.lastname) }} 19 | {{ form_errors(form.user.lastname) }} 20 |
21 |
22 |
23 | {{ form_label(form.user.firstname, null, { 'label_attr': {'class': 'control-label'} }) }} 24 |
25 | {{ form_widget(form.user.firstname) }} 26 | {{ form_errors(form.user.firstname) }} 27 |
28 |
29 |
30 | {{ form_label(form.member.username, null, { 'label_attr': {'class': 'control-label'} }) }} 31 |
32 | {{ form_widget(form.member.username, { 'attr': {'onchange': 'checkUsername(this)'} }) }} 33 | {{ form_errors(form.member.username) }} 34 |
35 |
36 |
37 | {{ form_label(form.user.email, null, { 'label_attr': {'class': 'control-label'} }) }} 38 |
39 |
40 | 41 | {{ form_widget(form.user.email, { 'attr': {'class': 'span2'} }) }} 42 | {{ form_errors(form.user.email) }} 43 |
44 |
45 |
46 |
47 | {{ form_label(form.member.password, null, { 'label_attr': {'class': 'control-label'} }) }} 48 |
49 | {{ form_widget(form.member.password) }} 50 | {{ form_errors(form.member.password) }} 51 |
52 |
53 | {{ form_rest(form) }} 54 |
55 | 56 | 57 |
58 |
59 |
60 |
61 |
62 |
63 | {% endblock %} 64 | 65 | {% block bottomjs %} 66 | 81 | {% endblock %} 82 | -------------------------------------------------------------------------------- /tests/units/Aperophp/Meetup/UserTransformer.php: -------------------------------------------------------------------------------- 1 | 13 | array ( 14 | 'country' => 'fr', 15 | 'localized_country_name' => 'France', 16 | 'city' => 'Villleurbanne', 17 | 'address_1' => '26 rue Louis Guérin', 18 | 'address_2' => '1er étage', 19 | 'name' => 'Amabla - Elao', 20 | 'lon' => 4.8601939999999999, 21 | 'id' => 23511318, 22 | 'lat' => 45.777003999999998, 23 | 'repinned' => false, 24 | ), 25 | 'created' => 1449093417000, 26 | 'response' => 'yes', 27 | 'member_photo' => 28 | array ( 29 | 'highres_link' => 'http://photos1.meetupstatic.com/photos/member/d/5/e/0/highres_208014752.jpeg', 30 | 'photo_id' => 208014752, 31 | 'photo_link' => 'http://photos1.meetupstatic.com/photos/member/d/5/e/0/member_208014752.jpeg', 32 | 'thumb_link' => 'http://photos1.meetupstatic.com/photos/member/d/5/e/0/thumb_208014752.jpeg', 33 | ), 34 | 'tallies' => 35 | array ( 36 | 'no' => 11, 37 | 'waitlist' => 0, 38 | 'maybe' => 0, 39 | 'yes' => 48, 40 | ), 41 | 'guests' => 0, 42 | 'member' => 43 | array ( 44 | 'member_id' => 99837872, 45 | 'name' => 'Adrien Gallou', 46 | ), 47 | 'rsvp_id' => 1583101090, 48 | 'mtime' => 1449093417000, 49 | 'event' => 50 | array ( 51 | 'name' => 'Ansible Lyon kickoff meeting', 52 | 'id' => '223238801', 53 | 'time' => 1450375200000, 54 | 'event_url' => 'http://www.meetup.com/Ansible-Lyon/events/223238801/', 55 | ), 56 | 'group' => 57 | array ( 58 | 'join_mode' => 'open', 59 | 'created' => 1457288152065, 60 | 'group_lon' => 4.8299999237060547, 61 | 'id' => 18672205, 62 | 'urlname' => 'Ansible-Lyon', 63 | 'group_lat' => 45.759998321533203, 64 | ), 65 | ); 66 | 67 | $expectedDrinkDescription = <<Bonjour à tous,

Nous avons le plaisir de vous annoncer l'organisation du premier Meetup Ansible Lyonnais !

En fonction du nombre de personnes présentes l'évènement aura lieu soit chez ELAO soit chez Amabla si nous sommes nombreux.

Au programme:

• 19h30 Émilien MANTEL : "Ansible et AWS, approche de l'autoscale"

•  20h10 : Guewen FAIVRE (ELAO) : "The automation journey of a web agency"

A tous les speakers intéressés n'hésitez pas à m'envoyer vos propositions de talks pour les futurs meetups ! 


Pizza et boissons vous attendent !

Sponsors : ELAO & Ansible 

--- 

Hi everyone ! 

We are pleased to announce the date of the first Ansible Lyon Meetup. According to how many "Ansi-bulls" we'll be, the event will be hosted by ELAO or Amabla.

69 | 70 | • 7h30 PM Émilien MANTEL : "Ansible and AWS, approach to auto-scaling"

• 8h10 PM : Guewen FAIVRE (ELAO) : "The automation journey of a web agency"

To every speakers, you can send me your suggestions for Talks, more meetups are comming !
Pizzas and drinks awaiting you :) Sponsors : ELAO & Ansible

71 | EOF; 72 | 73 | $this 74 | ->if($transformer = new \Aperophp\Meetup\UserTransformer()) 75 | ->then 76 | ->array($transformer->transform($rsvp)) 77 | ->integer['meetup_com_id']->isEqualTo(99837872) 78 | ->string['firstname']->isEqualTo("Adrien Gallou") 79 | ; 80 | 81 | $rsvpNo = $rsvp; 82 | $rsvp['response'] = 'no'; 83 | 84 | $this 85 | ->if($transformer = new \Aperophp\Meetup\UserTransformer()) 86 | ->then 87 | ->variable($transformer->transform($rsvp))->isNull(); 88 | ; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Resources/views/drink/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% form_theme form 'common/Form/fields.html.twig' %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 | 10 |

Editer l'apéro

11 |
12 |
13 |
14 | {{ form_errors(form) }} 15 |
16 |
17 | 18 |
19 | 20 |
21 |
22 |
23 | {{ form_label(form.hour, null, { 'label_attr': {'class': 'control-label'} }) }} 24 |
25 | {{ form_widget(form.hour) }} 26 | {{ form_errors(form.hour) }} 27 |
28 |
29 |
30 | {{ form_label(form.description, null, { 'label_attr': {'class': 'control-label'} }) }} 31 |
32 | {{ form_widget(form.description) }} 33 | {{ form_errors(form.description) }} 34 |
35 |
36 |
37 | {{ form_label(form.city_id, null, { 'label_attr': {'class': 'control-label'} }) }} 38 |
39 | {{ form_widget(form.city_id) }} 40 | {{ form_errors(form.city_id) }} 41 |
42 |
43 |
44 | {{ form_label(form.kind, null, { 'label_attr': {'class': 'control-label'} }) }} 45 |
46 | {{ form_widget(form.kind) }} 47 | {{ form_errors(form.kind) }} 48 |
49 |
50 |
51 | 52 |
53 |

54 |

55 |

56 |
57 |
58 | {{ form_rest(form) }} 59 |
60 | 61 | 62 |
63 |
64 |
65 |
66 |
67 |
68 | {% endblock %} 69 | 70 | {% block bottomjs %} 71 | 72 | 73 | 74 | 79 | {% endblock %} 80 | -------------------------------------------------------------------------------- /src/Aperophp/Provider/Controller/Comment.php: -------------------------------------------------------------------------------- 1 | 13 | * @since 18 févr. 2012 14 | * @version 1.0 - 18 févr. 2012 - Koin 15 | */ 16 | class Comment implements ControllerProviderInterface 17 | { 18 | public function connect(Application $app) 19 | { 20 | $controllers = $app['controllers_factory']; 21 | 22 | // ******* 23 | // ** Add a comment 24 | // ******* 25 | $controllers->post('{drinkId}/create.html', function(Request $request, $drinkId) use ($app) 26 | { 27 | $drink = $app['drinks']->find($drinkId); 28 | 29 | if (!$drink) { 30 | $app->abort(404, 'Cet événement n\'existe pas.'); 31 | } 32 | 33 | $form = $app['form.factory']->create('drink_comment'); 34 | 35 | $form->bind($request->request->get('drink_comment')); 36 | if ($form->isValid()) { 37 | $data = $form->getData(); 38 | 39 | // If user is not authenticated, a user is created. 40 | if (!$app['session']->has('user')) { 41 | try { 42 | $app['users']->insert($data['user']); 43 | $data['user']['id'] = $app['users']->lastInsertId(); 44 | $app['session']->set('user', $data['user']); 45 | } catch (\Exception $e) { 46 | $app->abort(500, 'Impossible de sauvegarder vos identifiants. Merci de réessayer plus tard.'); 47 | } 48 | } 49 | 50 | $user = $app['session']->get('user'); 51 | 52 | $authorName = $user['lastname']; 53 | $email = $user['email']; 54 | $isSpam = $app['akismet']->isSpam($data['content'], $authorName, $email, null, null, 'comment'); 55 | 56 | try { 57 | $app['drink_comments']->insert(array( 58 | 'content' => $data['content'], 59 | 'user_id' => $user['id'], 60 | 'drink_id' => $drinkId, 61 | 'is_spam' => $isSpam, 62 | )); 63 | } catch (\Exception $e) { 64 | $app->abort(500, 'Impossible de sauvegarder votre commentaire. Merci de réessayer plus tard.'); 65 | } 66 | 67 | $app['session']->getFlashBag()->add('success', 'Votre commentaire a été posté avec succès.'); 68 | 69 | return $request->isXmlHttpRequest() ? 'redirect' : $app->redirect($app['url_generator']->generate('_showdrink', array('id' => $drinkId))); 70 | } 71 | 72 | return $app['twig']->render('comment/_new.html.twig', array( 73 | 'commentForm' => $form->createView(), 74 | 'drink' => $drink, 75 | )); 76 | })->bind('_createcomment'); 77 | 78 | $controllers->get('{drinkId}/comment/{commentId}/toggle_spam.html', function(Request $request, $drinkId, $commentId) use ($app) { 79 | $drinkComment = $app['drink_comments']->find($commentId); 80 | if (null == $drinkComment) { 81 | $app->abort(500, 'Commentaire non existant.'); 82 | } 83 | $drink = $app['drinks']->find($drinkId); 84 | if (null == $drink) { 85 | $app->abort(500, 'Drink non existant.'); 86 | } 87 | $member = $app['session']->get('member'); 88 | if (!$member) { 89 | $app['session']->getFlashBag()->add('error', 'Vous devez être authentifié pour pouvoir éditer cet événement.'); 90 | 91 | return $app->redirect($app['url_generator']->generate('_signinmember')); 92 | } 93 | 94 | if ($drink['member_id'] != $member['id']) { 95 | $app['session']->getFlashBag()->add('error', "Vous devez être organisateur de cet événement pour pouvoir modifier l'état des commentaires."); 96 | 97 | return $app->redirect($app['url_generator']->generate('_showdrink', array('id' => $drinkId))); 98 | } 99 | $drinkComment['is_spam'] = !$drinkComment['is_spam']; 100 | $app['drink_comments']->update($drinkComment, array('id' => $commentId)); 101 | return $app->redirect($app['url_generator']->generate('_showdrink', array('id' => $drinkId))); 102 | })->bind('_comment_toggle_is_spam'); 103 | // ******* 104 | 105 | return $controllers; 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/Aperophp/Command/SyncWithMeetup.php: -------------------------------------------------------------------------------- 1 | meetupClient = $meetupClient; 59 | $this->drinkRepository = $drink; 60 | $this->cityRepositories = $city; 61 | $this->userRepository = $user; 62 | $this->drinkParticipantRepository = $drinkParticipant; 63 | $this->groupUrlnames = $groupUrlnames; 64 | 65 | } 66 | 67 | protected function configure() 68 | { 69 | $this 70 | ->setName('sync-with-meetup') 71 | ->setDescription('Sync drinks with meetup.com') 72 | ; 73 | } 74 | 75 | /** 76 | * @param InputInterface $input 77 | * @param OutputInterface $output 78 | * 79 | * @return int|null|void 80 | * 81 | * @throws \Exception 82 | */ 83 | protected function execute(InputInterface $input, OutputInterface $output) 84 | { 85 | $command = $this->meetupClient->getCommand( 86 | 'GetEvents', 87 | array('group_urlname' => implode(',', $this->groupUrlnames)) 88 | ); 89 | 90 | $command->prepare(); 91 | $events = $command->execute(); 92 | 93 | $eventTransformer = new EventTransformer($this->cityRepositories->findAllInAssociativeArray()); 94 | $userTransformer = new UserTransformer(); 95 | 96 | foreach ($events as $event) { 97 | $drink = $eventTransformer->transform($event); 98 | 99 | if (null === $drink) { 100 | continue; 101 | } 102 | 103 | if (false === ($foundDrink = $this->drinkRepository->findByMeetupId($drink['meetup_com_id']))) { 104 | $this->drinkRepository->insert($drink); 105 | } else { 106 | $this->drinkRepository->update($drink, array('meetup_com_id' => $drink['meetup_com_id'])); 107 | } 108 | $drink = $this->drinkRepository->findByMeetupId($drink['meetup_com_id']); 109 | 110 | $command = $this->meetupClient->getCommand('getRsvps', array('event_id' => $drink['meetup_com_id'])); 111 | $command->prepare(); 112 | $rsvps = $command->execute(); 113 | 114 | $this->drinkParticipantRepository->delete(array( 115 | 'drink_id' => $drink['id'], 116 | )); 117 | 118 | foreach ($rsvps as $rsvp) { 119 | $user =$userTransformer->transform($rsvp); 120 | 121 | if (null === $user) { 122 | continue; 123 | } 124 | 125 | if (false === ($foundUser = $this->userRepository->findByMeetupId($user['meetup_com_id']))) { 126 | $this->userRepository->insert($user); 127 | } else { 128 | $this->userRepository->update($user, array('meetup_com_id' => $user['meetup_com_id'])); 129 | } 130 | $user = $this->userRepository->findByMeetupId($user['meetup_com_id']); 131 | 132 | $drinkParticipant = array( 133 | 'drink_id' => $drink['id'], 134 | 'user_id' => $user['id'], 135 | 'percentage' => 100, 136 | 'reminder' => 0, 137 | ); 138 | 139 | $this->drinkParticipantRepository->insert($drinkParticipant); 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Aperophp/Form/Type/DrinkType.php: -------------------------------------------------------------------------------- 1 | 17 | * @since 6 févr. 2012 18 | * @version 1.1 - 18 févr. 2012 - Koin 19 | */ 20 | class DrinkType extends AbstractType 21 | { 22 | protected $cityRepository; 23 | protected $cities = null; 24 | protected $drinkRepository; 25 | protected $kinds = null; 26 | 27 | public function __construct(Repository\City $cityRepository, Repository\Drink $drinkRepository) 28 | { 29 | $this->cityRepository = $cityRepository; 30 | $this->drinkRepository = $drinkRepository; 31 | } 32 | 33 | public function buildForm(FormBuilderInterface $builder, array $options) 34 | { 35 | $builder->addEventSubscriber(new DataFilterSubscriber($builder)); 36 | 37 | $builder 38 | ->add('captcha', 'hidden') 39 | ->add('place', 'hidden') 40 | ->add('address', 'hidden') 41 | ->add('latitude', 'hidden') 42 | ->add('longitude', 'hidden') 43 | ->add('day', 'hidden') 44 | ->add('hour', 'choice', array( 45 | 'label' => 'Heure', 46 | 'choices' => $options['hours'], 47 | 'data' => '19:00:00' 48 | )) 49 | ->add('city_id', 'choice', array( 50 | 'label' => 'Ville', 51 | 'choices' => $options['cities'] 52 | )) 53 | ->add('kind', 'choice', array( 54 | 'label' => 'Type', 55 | 'choices' => $options['kinds'] 56 | )) 57 | ->add('description', 'textarea', array( 58 | 'label' => 'Description' 59 | )); 60 | } 61 | 62 | public function setDefaultOptions(OptionsResolverInterface $resolver) 63 | { 64 | // Collection Constraint 65 | $collectionConstraint = new Constraints\Collection(array( 66 | 'fields' => array( 67 | 'captcha' => new Constraints\Blank(array('message' => "Vous avez été détecté comme un robot.")), 68 | 'place' => array( 69 | new Constraints\NotNull(), 70 | new Constraints\Length(array('max' => 100)), 71 | ), 72 | 'address' => new Constraints\Length(array('max' => 100)), 73 | 'latitude' => new Constraints\NotNull(), // TODO use a DataTransformer to validate theses 2 values 74 | 'longitude' => new Constraints\NotNull(), 75 | 'day' => array( 76 | new Constraints\NotNull(), 77 | new AperophpConstraints\FutureDate(), 78 | ), 79 | 'hour' => array( 80 | new Constraints\NotNull(), 81 | new Constraints\Time(), 82 | ), 83 | 'city_id' => array( 84 | new Constraints\NotNull(), 85 | new Constraints\Choice(array( 86 | 'choices' => array_keys($this->getCities()) 87 | )), 88 | ), 89 | 'kind' => array( 90 | new Constraints\NotNull(), 91 | new Constraints\Choice(array( 92 | 'choices' => array_keys($this->getKinds()) 93 | )), 94 | ), 95 | 'description' => new Constraints\NotNull(), 96 | ), 97 | 'allowExtraFields' => false, 98 | )); 99 | 100 | // Hours 101 | $hours = array(); 102 | $oStartDate = new \DateTime('2000-01-01'); 103 | $oEndDate = new \DateTime('2000-01-02'); 104 | 105 | do { 106 | $hours[$oStartDate->format('H:i:s')] = $oStartDate->format('H\hi'); 107 | $oStartDate->add(new \DateInterval('PT30M')); 108 | } while ($oStartDate < $oEndDate); 109 | 110 | $resolver->setDefaults(array( 111 | 'validation_constraint' => $collectionConstraint, 112 | 'hours' => $hours, 113 | 'cities' => $this->getCities(), 114 | 'kinds' => $this->getKinds(), 115 | )); 116 | } 117 | 118 | public function getName() 119 | { 120 | return 'drink'; 121 | } 122 | 123 | protected function getCities() 124 | { 125 | if (null === $this->cities) { 126 | $this->cities = $this->cityRepository->findAllInAssociativeArray(); 127 | } 128 | 129 | return $this->cities; 130 | } 131 | 132 | protected function getKinds() 133 | { 134 | if (null === $this->kinds) { 135 | $this->kinds = $this->drinkRepository->findAllKindsInAssociativeArray(); 136 | } 137 | 138 | return $this->kinds; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /app/bootstrap.php: -------------------------------------------------------------------------------- 1 | share(function() use ($app) { 31 | return new \Aperophp\Lib\Utils($app); 32 | }); 33 | 34 | $app['mail_factory'] = $app->share(function() use ($app) { 35 | return new \Aperophp\Lib\MailFactory($app['mailer'], $app['twig']); 36 | }); 37 | 38 | $app->register(new UrlGeneratorServiceProvider()); 39 | 40 | $app->register(new FormServiceProvider()); 41 | $app['form.extensions'] = $app->share($app->extend('form.extensions', function ($extensions) use ($app) { 42 | $extensions[] = new Aperophp\Form\FormExtension($app); 43 | 44 | return $extensions; 45 | })); 46 | 47 | $app->register(new ValidatorServiceProvider()); 48 | 49 | $app->register(new SessionServiceProvider(), array( 50 | 'locale' => $app['locale'], 51 | 'session.storage.options' => array( 52 | 'auto_start' => true 53 | ), 54 | )); 55 | 56 | $app->register(new DoctrineServiceProvider()); 57 | 58 | $app['repository.repositories'] = array( 59 | 'cities' => 'Aperophp\Repository\City', 60 | 'drinks' => 'Aperophp\Repository\Drink', 61 | 'drink_comments' => 'Aperophp\Repository\DrinkComment', 62 | 'drink_participants' => 'Aperophp\Repository\DrinkParticipant', 63 | 'members' => 'Aperophp\Repository\Member', 64 | 'users' => 'Aperophp\Repository\User', 65 | ); 66 | $app->register(new Aperophp\Provider\RepositoryServiceProvider()); 67 | 68 | // ******* 69 | // ** Twig 70 | // ******* 71 | $app->register(new TwigServiceProvider(), array( 72 | 'twig.options' => array( 73 | 'debug' => $app['debug'] 74 | ), 75 | 'twig.path' => array(__DIR__ . '/../src/Resources/views') 76 | )); 77 | 78 | // Add Twig extensions 79 | $app['twig'] = $app->share($app->extend('twig', function($twig, $app) { 80 | $twig->addExtension(new Twig_Extensions_Extension_Debug()); 81 | $twig->addExtension(new Twig_Extensions_Extension_Text()); 82 | $twig->addGlobal('ga_enabled', $app['ga.enabled']); 83 | $twig->addGlobal('ga_ua', $app['ga.ua']); 84 | 85 | return $twig; 86 | })); 87 | // ******* 88 | 89 | 90 | // ******* 91 | // ** Translations 92 | // ******* 93 | $app->register(new TranslationServiceProvider(array( 94 | 'locale_fallback' => 'fr', 95 | 'locale' => $app['locale'], 96 | ))); 97 | 98 | $app['translator.domains'] = array( 99 | 'messages' => array( 100 | 'fr' => array( 101 | 'January' => 'Janvier', 102 | 'February' => 'Février', 103 | 'March' => 'Mars', 104 | 'April' => 'Avril', 105 | 'May' => 'Mai', 106 | 'June' => 'Juin', 107 | 'July' => 'Juillet', 108 | 'August' => 'Août', 109 | 'September' => 'Septembre', 110 | 'October' => 'Octobre', 111 | 'November' => 'Novembre', 112 | 'December' => 'Décembre', 113 | 'Monday' => 'Lundi', 114 | 'Tuesday' => 'Mardi', 115 | 'Wednesday' => 'Mercredi', 116 | 'Thursday' => 'Jeudi', 117 | 'Friday' => 'Vendredi', 118 | 'Saturday' => 'Samedi', 119 | 'Sunday' => 'Dimanche', 120 | 'drink' => 'Apéro', 121 | 'drink' => 'Apéro', 122 | 'talk' => 'Mini-conf', 123 | 'For sure, I will be there' => 'Présence assurée', 124 | 'I will probably be there' => 'Devrait être là', 125 | 'I will try to be there' => 'Essayera d\'être là', 126 | 'I won\'t be there' => 'Ne sera pas là', 127 | ) 128 | ), 129 | 'validators' => array( 130 | 'fr' => array( 131 | 'The date must be in the future' => 'La date doit être future', 132 | ) 133 | ) 134 | ); 135 | // ******* 136 | 137 | // ******* 138 | // ** Mail 139 | // ******* 140 | $app->register(new SwiftmailerServiceProvider(), array( 141 | 'swiftmailer.options' => $app['mail.options'], 142 | 'swiftmailer.class_path' => __DIR__.'/../vendor/SwiftMailer/lib/classes/', 143 | )); 144 | // ******* 145 | 146 | // ******* 147 | // ** Gravatar 148 | // ******* 149 | $app->register(new SilexGravatar\GravatarExtension(), array( 150 | 'gravatar.class_path' => __DIR__ . '/../vendor/mheap/Silex-Gravatar/src', 151 | 'gravatar.cache_dir' => __DIR__ . '/../cache', 152 | 'gravatar.cache_ttl' => 240, // 240 seconds 153 | 'gravatar.options' => array( 154 | 'size' => 100, 155 | 'rating' => Gravatar\Service::RATING_G, 156 | 'secure' => true, 157 | 'default' => Gravatar\Service::DEFAULT_MM, 158 | 'force_default' => false 159 | ) 160 | )); 161 | // ******* 162 | 163 | 164 | $app['twig']->addExtension(new \Aperophp\Lib\AutoLinkTwigExtension()); 165 | 166 | 167 | $app['akismet'] = $app->share(function() use ($app) { 168 | return new \TijsVerkoyen\Akismet\Akismet($app['akismet_api_key'], $app['akismet_url']); 169 | }); 170 | 171 | $app['meetup_client'] = $app->share(function() use ($app) { 172 | return \DMS\Service\Meetup\MeetupKeyAuthClient::factory(array('key' => $app['meetup.api_key'])); 173 | }); 174 | 175 | return $app; 176 | -------------------------------------------------------------------------------- /tests/units/Aperophp/Meetup/EventTransformer.php: -------------------------------------------------------------------------------- 1 | Bonjour à tous,

Nous avons le plaisir de vous annoncer l'organisation du premier Meetup Ansible Lyonnais !

En fonction du nombre de personnes présentes l'évènement aura lieu soit chez ELAO soit chez Amabla si nous sommes nombreux.

Au programme:

• 19h30 Émilien MANTEL : "Ansible et AWS, approche de l'autoscale"

•  20h10 : Guewen FAIVRE (ELAO) : "The automation journey of a web agency"

A tous les speakers intéressés n'hésitez pas à m'envoyer vos propositions de talks pour les futurs meetups ! 


Pizza et boissons vous attendent !

Sponsors : ELAO & Ansible 

--- 

Hi everyone ! 

We are pleased to announce the date of the first Ansible Lyon Meetup. According to how many "Ansi-bulls" we'll be, the event will be hosted by ELAO or Amabla.

13 | 14 | • 7h30 PM Émilien MANTEL : "Ansible and AWS, approach to auto-scaling"

• 8h10 PM : Guewen FAIVRE (ELAO) : "The automation journey of a web agency"

To every speakers, you can send me your suggestions for Talks, more meetups are comming !
Pizzas and drinks awaiting you :) Sponsors : ELAO & Ansible

15 | EOF; 16 | 17 | $this 18 | ->if($transformer = new \Aperophp\Meetup\EventTransformer($this->getBaseTestCities())) 19 | ->then 20 | ->array($transformer->transform($this->getBaseTestEvent(array()))) 21 | ->integer['city_id']->isEqualTo(126) 22 | ->string['place']->isEqualTo('26 rue Louis Guérin') 23 | ->string['address']->isEqualTo('26 rue Louis Guérin Lyon, France') 24 | ->float['latitude']->isEqualTo('45.777004') 25 | ->float['longitude']->isEqualTo('4.860194') 26 | ->string['description']->isEqualTo($expectedDrinkDescription) 27 | ->string['day']->isEqualTo('2015-12-17') 28 | ->string['hour']->isEqualTo('19:00') 29 | ->string['created_at']->isEqualTo('2015-06-14 17:57:39') 30 | ->string['updated_at']->isEqualTo('2015-12-18 18:28:57') 31 | ->string['meetup_com_id']->isEqualTo('223238801') 32 | ->string['meetup_com_event_url']->isEqualTo('http://www.meetup.com/Ansible-Lyon/events/223238801/') 33 | ; 34 | } 35 | 36 | public function testEventNonApero() 37 | { 38 | $this 39 | ->if($transformer = new \Aperophp\Meetup\EventTransformer($this->getBaseTestCities())) 40 | ->then 41 | ->variable($transformer->transform($this->getBaseTestEvent(array( 42 | 'name' => 'Session de lightning talks', 43 | )))) 44 | ->isNull() 45 | ; 46 | } 47 | 48 | protected function getBaseTestCities() 49 | { 50 | return array( 51 | 125 => 'Luxembourg', 52 | 126 => 'Lyon', 53 | 127 => 'Marcq-En-Baroeul', 54 | ); 55 | } 56 | 57 | protected function getBaseTestEvent(array $customData) 58 | { 59 | return array_merge(array ( 60 | 'utc_offset' => 3600000, 61 | 'venue' => 62 | array ( 63 | 'country' => 'fr', 64 | 'localized_country_name' => 'France', 65 | 'city' => 'Lyon', 66 | 'address_1' => '26 rue Louis Guérin', 67 | 'address_2' => '1er étage', 68 | 'name' => 'Amabla - Elao', 69 | 'lon' => 4.8601939999999999, 70 | 'id' => 23511318, 71 | 'lat' => 45.777003999999998, 72 | 'repinned' => false, 73 | ), 74 | 'headcount' => 50, 75 | 'visibility' => 'public', 76 | 'waitlist_count' => 0, 77 | 'created' => 1434297459000, 78 | 'rating' => 79 | array ( 80 | 'count' => 4, 81 | 'average' => 4, 82 | ), 83 | 'maybe_rsvp_count' => 0, 84 | 'description' => '

Bonjour à tous,

Nous avons le plaisir de vous annoncer l\'organisation du premier Meetup Ansible Lyonnais !

En fonction du nombre de personnes présentes l\'évènement aura lieu soit chez ELAO soit chez Amabla si nous sommes nombreux.

Au programme:

• 19h30 Émilien MANTEL : "Ansible et AWS, approche de l\'autoscale"

•  20h10 : Guewen FAIVRE (ELAO) : "The automation journey of a web agency"

A tous les speakers intéressés n\'hésitez pas à m\'envoyer vos propositions de talks pour les futurs meetups ! 


Pizza et boissons vous attendent !

Sponsors : ELAO & Ansible 

--- 

Hi everyone ! 

We are pleased to announce the date of the first Ansible Lyon Meetup. According to how many "Ansi-bulls" we\'ll be, the event will be hosted by ELAO or Amabla.

85 | 86 | • 7h30 PM Émilien MANTEL : "Ansible and AWS, approach to auto-scaling"

• 8h10 PM : Guewen FAIVRE (ELAO) : "The automation journey of a web agency"

To every speakers, you can send me your suggestions for Talks, more meetups are comming !
Pizzas and drinks awaiting you :) Sponsors : ELAO & Ansible

', 87 | 'event_url' => 'http://www.meetup.com/Ansible-Lyon/events/223238801/', 88 | 'yes_rsvp_count' => 48, 89 | 'name' => 'Apéro PHP', 90 | 'id' => '223238801', 91 | 'time' => 1450375200000, 92 | 'updated' => 1450459737000, 93 | 'group' => 94 | array ( 95 | 'join_mode' => 'open', 96 | 'created' => 1434297356000, 97 | 'name' => 'Ansible Lyon', 98 | 'group_lon' => 4.8299999237060547, 99 | 'id' => 18672205, 100 | 'urlname' => 'Ansible-Lyon', 101 | 'group_lat' => 45.759998321533203, 102 | 'who' => 'Ansi-gônes', 103 | ), 104 | 'status' => 'past', 105 | ), $customData); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Aperophp/Lib/Stats.php: -------------------------------------------------------------------------------- 1 | db = $db; 34 | $this->dateFrom = $dateFrom; 35 | $this->city = $city; 36 | } 37 | 38 | /** 39 | * @param string $type 40 | * 41 | * @return null|string 42 | */ 43 | public static function getDateFrom($type) 44 | { 45 | $dateFrom = null; 46 | $date = new \DateTime(); 47 | if ($type == 'year') { 48 | $date->modify('-1 year'); 49 | $dateFrom = $date->format('Y-m-d'); 50 | } elseif ($type == 'month') { 51 | $date->modify('-6 month'); 52 | $dateFrom = $date->format('Y-m-d'); 53 | } 54 | 55 | return $dateFrom; 56 | } 57 | 58 | /** 59 | * @return array 60 | */ 61 | public static function getTypes() 62 | { 63 | return array( 64 | 'all' => 'Toutes', 65 | 'year' => 'Depuis un an', 66 | 'month' => 'Depuis 6 mois', 67 | ); 68 | } 69 | 70 | /** 71 | * @return \Doctrine\DBAL\Query\QueryBuilder 72 | */ 73 | protected function getBaseDrinkQuery() 74 | { 75 | $dateFrom = $this->dateFrom; 76 | $city = $this->city; 77 | $queryBuilder = $this->db->createQueryBuilder() 78 | ->from('Drink', 'd') 79 | ; 80 | if (null !== $dateFrom) { 81 | $queryBuilder->andWhere('day > :datefrom'); 82 | $queryBuilder->setParameter('datefrom', $dateFrom); 83 | } 84 | if ($city != City::ALL) { 85 | $queryBuilder->andWhere(sprintf('city_id = %s', $city)); 86 | } 87 | 88 | return $queryBuilder; 89 | } 90 | 91 | /** 92 | * @return int 93 | */ 94 | public function getCount() 95 | { 96 | $queryBuilder = $this->getBaseDrinkQuery() 97 | ->select('count(d.id) as count') 98 | ; 99 | 100 | return $queryBuilder->execute()->fetchColumn(); 101 | } 102 | 103 | /** 104 | * @param bool $onlyRecurrentCities 105 | * 106 | * @return array 107 | */ 108 | public function averageParticipantsByCity($onlyRecurrentCities = false) 109 | { 110 | $queryBuilder = $this->getBaseDrinkQuery() 111 | ->addSelect(sprintf("CEILING(AVG((%s))) as participants_avg", Drink::getCountParticipantsQuery())) 112 | ->addSelect('COUNT(d.id) as total_drinks') 113 | ->addSelect('c.name as name') 114 | ->innerJoin('d', 'City', 'c', 'd.city_id = c.id') 115 | ->addGroupBy('c.id') 116 | ->addOrderBy('participants_avg', 'DESC') 117 | ->addOrderBy('name') 118 | ; 119 | 120 | if ($onlyRecurrentCities) { 121 | $queryBuilder->andHaving('total_drinks > :recurrent_minimum'); 122 | $queryBuilder->setParameter('recurrent_minimum', self::RECURRENT_MINIMUM); 123 | } 124 | 125 | return $queryBuilder->execute()->fetchAll(); 126 | } 127 | 128 | /** 129 | * @return int 130 | */ 131 | public function countAllParticipants() 132 | { 133 | $queryBuilder = $this->getBaseDrinkQuery() 134 | ->addSelect('count(*) as count') 135 | ->innerJoin('d', 'Drink_Participation', 'dp', 'dp.drink_id = d.id') 136 | ->andWhere('dp.percentage > 0') 137 | ; 138 | 139 | return $queryBuilder->execute()->fetchColumn(); 140 | } 141 | 142 | /** 143 | * @return array 144 | */ 145 | public function countParticipantsByDate() 146 | { 147 | $queryBuilder = $this->getBaseDrinkQuery() 148 | ->addSelect('count(*) as count') 149 | ->addSelect('d.day as day') 150 | ->innerJoin('d', 'Drink_Participation', 'dp', 'dp.drink_id = d.id') 151 | ->andWhere('dp.percentage > 0') 152 | ->addGroupBy('day') 153 | ; 154 | 155 | $dates = array(); 156 | 157 | foreach ($queryBuilder->execute() as $row) { 158 | $dates[$row['day']] = $row['count']; 159 | } 160 | 161 | return $dates; 162 | } 163 | 164 | /** 165 | * @return array 166 | */ 167 | public function getGeoInformations() 168 | { 169 | $queryBuilder = $this->getBaseDrinkQuery() 170 | ->addSelect('latitude', 'longitude', 'description') 171 | ->addGroupBy('d.id') 172 | ->addOrderBy('created_at', 'DESC') 173 | ; 174 | return $queryBuilder->execute()->fetchAll(); 175 | } 176 | 177 | /** 178 | * @return array 179 | */ 180 | public function findFirst() 181 | { 182 | $queryBuilder = $this->getBaseDrinkQuery() 183 | ->select('*') 184 | ->addOrderBy('day') 185 | ->addOrderBy('hour') 186 | ->addOrderBy('created_at') 187 | ->setMaxResults(1) 188 | ; 189 | 190 | return $queryBuilder->execute()->fetch(); 191 | } 192 | 193 | /** 194 | * @return mixed 195 | */ 196 | public function getMostUSedMonth() 197 | { 198 | return $this->getBaseDrinkQuery() 199 | ->addSelect('day') 200 | ->addOrderBy('COUNT(*)') 201 | ->innerJoin('d', 'Drink_Participation', 'dp', 'dp.drink_id = d.id') 202 | ->andWhere('dp.percentage > 0') 203 | ->addGroupBy('MONTH(day)') 204 | ->setMaxResults(1) 205 | ->execute() 206 | ->fetchColumn() 207 | ; 208 | } 209 | 210 | /** 211 | * @return int 212 | */ 213 | public function getAverageParticipants() 214 | { 215 | return $this->getBaseDrinkQuery() 216 | ->select(sprintf("CEILING(AVG((%s))) as participants_avg", Drink::getCountParticipantsQuery())) 217 | ->innerJoin('d', 'Drink_Participation', 'dp', 'dp.drink_id = d.id') 218 | ->execute() 219 | ->fetchColumn() 220 | ; 221 | } 222 | 223 | /** 224 | * @return int 225 | */ 226 | public function getMaxParticipants() 227 | { 228 | return $this->getBaseDrinkQuery() 229 | ->select(sprintf("MAX((%s)) as participants_avg", Drink::getCountParticipantsQuery())) 230 | ->innerJoin('d', 'Drink_Participation', 'dp', 'dp.drink_id = d.id') 231 | ->execute() 232 | ->fetchColumn() 233 | ; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/Resources/views/drink/view.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% block metas %} 4 | 5 | 6 | 7 | 8 | 9 | {% endblock%} 10 | 11 | {% block content %} 12 |
13 |
14 |
15 | 16 |

Les infos

17 | {% if not isFinished and (app.session.has('member')) %} 18 | 19 | 20 | {% endif %} 21 | 22 |
23 |
24 | {% if not isFinished %} 25 | {% if drink.meetup_com_id %} 26 | S'inscrire sur meetup.com 27 | {% else %} 28 | S'inscrire 29 | {% endif %} 30 | {% endif %} 31 | 32 |

{{ drink.kind|trans }} à {{ drink.city_name }}, le {{ drink.day|date("d") }} {{ drink.day|date("F")|trans|lower }} {{ drink.day|date("Y") }} à {{ drink.hour|date("H:i") }}

33 | 34 |

Lieu : {{ drink.place }}, {{ drink.address }}

35 |
36 |

{{ drink.description|autolink(1, 20)|raw }}

37 | organisé par {{ drink.organizer_username }} 38 |
39 |
40 |
41 |
42 |
43 | 44 |

Le lieu

45 |
46 |
47 |
48 | 51 |
52 |
53 |
54 |
55 |
56 |
57 | 58 |

Les participants

59 |
60 |
61 | {% include 'drink/_participations.html.twig' with { 'participants': participants, 'nb': drink.participants_count, 'isFinished': isFinished } %} 62 |
63 |
64 | {% if not drink.meetup_com_id %} 65 |
66 |
67 | 68 |

Les commentaires

69 |
70 |
71 | {% include 'comment/_list.html.twig' with { 72 | 'comments': comments, 73 | 'display_spam_buttons': (app.session.has('member') and app.session.get('member').id == drink.member_id), 74 | } %} 75 | Ajouter un commentaire 76 | 79 |
80 |
81 | {% endif %} 82 |
83 | {% endblock %} 84 | 85 | {% block bottomjs %} 86 | 87 | 101 | 163 | {% endblock %} 164 | -------------------------------------------------------------------------------- /src/Resources/views/stats/stats.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 |
8 | 9 |

Résumé

10 | 11 |
12 | 13 | 14 | 17 | 18 | 23 |
24 | 25 |
26 | 27 | 28 | 31 | 32 | 37 |
38 | 39 |
40 |
41 | {% if total %} 42 | Depuis le {{ date_from|date("d") }} {{ date_from|date("F")|trans|lower }} {{ date_from|date("Y") }} , {{ total }} apéros ont été organisés et ont réunis {{ total_participants|number_format(0, '', ' ') }} participants. Comment ceux-ci sont-ils répartis ? 43 | {% else %} 44 | Aucun résultat. 45 | {% endif %} 46 |
47 |
48 |
49 | 50 | {% if total and display_all_cities %} 51 |
52 |
53 |
54 | 55 |

Répartition géographique

56 |
57 |
58 |
59 |
60 |
61 |
62 | 63 | 64 |
65 |
66 |
67 | 68 |

Moyenne du nombre d'inscrits aux apéros, par ville

69 |
70 |
71 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | {% for city in avg_participants %} 93 | 94 | 95 | 96 | 97 | 98 | 99 | {% endfor %} 100 | 101 | 102 | 103 | 104 | {% if type == 'all' %} 105 | Seulement les villes ayant organisées plus de {{ constant('\\Aperophp\\Lib\\Stats::RECURRENT_MINIMUM') }} apéros sont affichées. 106 | {% endif %} 107 |
108 |
109 |
110 | 111 | {% endif %} 112 | 113 | {% if total %} 114 |
115 |
116 |
117 | 118 |

Nombre d'inscrits à travers le temps

119 |
120 |
121 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | {% for date, count in date_participants %} 140 | 141 | 142 | 143 | 144 | {% endfor %} 145 | 146 | 147 | 148 | 149 | 150 |
151 |
152 |
153 | 154 |
155 |
156 |
157 | 158 |

Statistiques diverses

159 |
160 |
161 |
    162 |
  • Moyenne de participants : {{ various.average }}
  • 163 |
  • Nombre maximum de participants : {{ various.max }}
  • 164 |
  • Les développeurs PHP préfèrent boire au mois de {{ various.month|date('F')|trans }}
  • 165 |
166 |
167 |
168 |
169 | 170 | {% endif %} 171 | 172 | {% endblock %} 173 | 174 | 175 | {% block bottomjs %} 176 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | {% endblock %} 187 | 188 | {% block headcss %} 189 | 190 | 191 | {% endblock %} 192 | -------------------------------------------------------------------------------- /assets/apero.less: -------------------------------------------------------------------------------- 1 | /* 2 | Document : apero 3 | Created on : 21 janv. 2012, 15:47:49 4 | Author : shaim 5 | Description: 6 | Purpose of the stylesheet follows. 7 | */ 8 | @grayDarker: #5779c5; 9 | @grayDark: #7f99d3; 10 | @textColor: #333333; 11 | @linkColor: #5779c5; 12 | @btnPrimaryBackground: @grayDark; 13 | @btnPrimaryBackgroundHighlight: @linkColor; 14 | 15 | body { 16 | font: 13px/1.7em 'Open Sans'; 17 | } 18 | 19 | p { 20 | font: 13px/1.7em 'Open Sans'; 21 | } 22 | 23 | input, 24 | button, 25 | select, 26 | textarea { 27 | font-family: 'Open Sans'; 28 | } 29 | 30 | .btn { 31 | color: #4269C1; 32 | } 33 | .btn-primary { 34 | color: @white; 35 | } 36 | .btn-icon-only { 37 | padding-right: 3px; 38 | padding-left: 3px; 39 | } 40 | 41 | .table td { 42 | vertical-align: middle; 43 | } 44 | 45 | .table-bordered th { 46 | #gradient > .vertical(#FAFAFA, #E9E9E9); 47 | font-size: 10px; 48 | color: #444; 49 | text-transform: uppercase; 50 | } 51 | 52 | .navbar .container { 53 | position: static; 54 | } 55 | .navbar-inner { 56 | padding: 7px 0; 57 | background: #7f99d3; 58 | border-bottom: 1px solid #5779C5; 59 | .border-radius(0); 60 | } 61 | .navbar-fixed-top { 62 | position: static; 63 | } 64 | .navbar .nav a { 65 | font-size: 12px; 66 | } 67 | .navbar .nav > li > a { 68 | color:#eee; 69 | } 70 | .navbar .brand { 71 | font-weight: 600; 72 | position: relative; 73 | top: 2px; 74 | color:#fff; 75 | } 76 | .navbar .search-query { 77 | background-color: #444; 78 | width: 150px; 79 | font-size: 11px; 80 | font-weight: bold; 81 | } 82 | .navbar .search-query::-webkit-input-placeholder { 83 | color: #666; 84 | } 85 | .navbar .search-query:-moz-placeholder { 86 | color: #666; 87 | } 88 | 89 | .subnavbar { 90 | margin-bottom: 2.5em; 91 | border-top: 1px solid @grayDark; 92 | } 93 | .subnavbar-inner { 94 | height: 80px; 95 | background: #3A3A3A; 96 | #gradient > .vertical(@grayDark, @grayDarker); 97 | border-bottom: 1px solid #000; 98 | } 99 | .subnavbar .container > ul { 100 | display: inline-block; 101 | height: 80px; 102 | padding: 0; 103 | margin: 0; 104 | border-left: 1px solid @grayDarker; 105 | border-right: 1px solid @grayDark; 106 | } 107 | .subnavbar .container > ul > li { 108 | float: left; 109 | min-width: 90px; 110 | height: 80px; 111 | padding: 0; 112 | margin: 0; 113 | text-align: center; 114 | list-style: none; 115 | border-left: 1px solid @grayDark; 116 | border-right: 1px solid @grayDarker; 117 | } 118 | .subnavbar .container > ul > li > a { 119 | display: block; 120 | 121 | height: 100%; 122 | padding: 0 15px; 123 | 124 | 125 | font-size: 12px; 126 | font-weight: bold; 127 | color: #fff; 128 | 129 | text-shadow: 1px 1px 1px rgba(0,0,0,.15); 130 | } 131 | .subnavbar .container > ul > li > a:hover { 132 | color: #ddd; 133 | text-decoration: none; 134 | } 135 | .subnavbar .container > ul > li > a > i { 136 | display: inline-block; 137 | width: 24px; 138 | height: 24px; 139 | margin-top: 17px; 140 | margin-bottom: .25em; 141 | font-size: 28px; 142 | } 143 | .subnavbar .container > ul > li > a > span { 144 | display: block; 145 | } 146 | .subnavbar .container > ul > li.active > a { 147 | background: #fff; 148 | color: @grayDark; 149 | } 150 | 151 | .widget { 152 | position: relative; 153 | clear: both; 154 | width: auto; 155 | margin-bottom: 2em; 156 | overflow: hidden; 157 | } 158 | .widget-header { 159 | position: relative; 160 | height: 40px; 161 | line-height: 40px; 162 | #gradient > .vertical(#FAFAFA, #E9E9E9); 163 | border: 1px solid #D5D5D5; 164 | border-top-left-radius: 4px; 165 | border-top-right-radius: 4px; 166 | -webkit-background-clip: padding-box; 167 | } 168 | .widget-header h3 { 169 | position: relative; 170 | top: 2px; 171 | left: 10px; 172 | display: inline-block; 173 | margin-right: 3em; 174 | font-size: 14px; 175 | font-weight: 800; 176 | color: #555; 177 | line-height: 18px; 178 | text-shadow: 1px 1px 2px rgba(255,255,255,.5); 179 | } 180 | .widget-header [class^="icon-"], .widget-header [class*=" icon-"] { 181 | display: inline-block; 182 | margin-left: 13px; 183 | margin-right: -2px; 184 | font-size: 16px; 185 | color: #555; 186 | vertical-align: middle; 187 | } 188 | .widget-content { 189 | padding: 20px 15px 15px; 190 | background: #FFF; 191 | border: 1px solid #D5D5D5; 192 | .border-radius(5px); 193 | } 194 | .widget-header+.widget-content { 195 | border-top: none; 196 | .border-radius(0px); 197 | } 198 | .widget-nopad .widget-content { 199 | padding: 0; 200 | } 201 | .widget-nopad form, .modal form { 202 | margin: 18px 0 0; 203 | } 204 | .widget-nopad .form-actions { 205 | margin-bottom: 0; 206 | } 207 | /* Widget Content Clearfix */ 208 | .widget-content:before, 209 | .widget-content:after { 210 | content:""; 211 | display:table; 212 | } 213 | .widget-content:after { 214 | clear:both; 215 | } 216 | /* For IE 6/7 (trigger hasLayout) */ 217 | .widget-content { 218 | zoom:1; 219 | } 220 | /* Widget Table */ 221 | .widget-table .widget-content { 222 | padding: 0; 223 | } 224 | .widget-table .table { 225 | margin-bottom: 0; 226 | border: none; 227 | } 228 | .widget-table .table tr td:first-child { 229 | border-left: none; 230 | } 231 | .widget-table .table tr th:first-child { 232 | border-left: none; 233 | } 234 | /* Widget Plain */ 235 | .widget-plain { 236 | background: transparent; 237 | border: none; 238 | } 239 | 240 | .widget-plain .widget-content { 241 | padding: 0; 242 | background: transparent; 243 | border: none; 244 | } 245 | /* Widget Box */ 246 | .widget-box .widget-content { 247 | background: #E3E3E3; 248 | background: #FFF; 249 | } 250 | 251 | .action-table .btn-small { 252 | padding: 4px 5px 5px; 253 | } 254 | .action-table .td-actions { 255 | text-align: center; 256 | } 257 | .action-table .td-actions .btn { 258 | margin-right: .5em; 259 | } 260 | .action-table .td-actions .btn:last-child { 261 | margin-rigth: 0; 262 | } 263 | 264 | .news-items { 265 | margin: 1em 0 0; 266 | } 267 | .news-items li { 268 | display: table; 269 | padding: 0 2em 0 1.5em; 270 | padding-bottom: 1em; 271 | margin-bottom: 1em; 272 | border-bottom: 1px dotted #CCC; 273 | } 274 | .news-items li:last-child { 275 | padding-bottom: 0; border: none; 276 | } 277 | .news-item-date { 278 | display: table-cell; 279 | } 280 | .news-item-detail { 281 | display: table-cell; 282 | width: 100%; 283 | } 284 | .news-item-title { 285 | font-size: 13px; 286 | font-weight: 600; 287 | } 288 | .news-item-date { 289 | width: 75px; 290 | vertical-align: middle; 291 | text-align: center; 292 | } 293 | .news-item-day { 294 | display: block; 295 | margin-bottom: .25em; 296 | font-size: 24px; 297 | color: #888; 298 | } 299 | .news-item-preview { 300 | margin-bottom: 0; 301 | color: #777; 302 | width: 100%; 303 | } 304 | .news-item-month { 305 | display: block; 306 | padding-right: 1px; 307 | font-size: 12px; 308 | font-weight: 600; 309 | color: #888; 310 | } 311 | .news-item-hour { 312 | display: block; 313 | margin-bottom: .25em; 314 | font-size: 20px; 315 | color: #888; 316 | } 317 | 318 | .modal form { 319 | margin: 0; 320 | } 321 | 322 | div.apero{ 323 | border: 1px #f9ab1a solid; 324 | padding: 1ex; 325 | .border-radius(6); 326 | } 327 | 328 | div.conference{ 329 | border: 1px #4c5e96 solid; 330 | padding: 1ex; 331 | .border-radius(6); 332 | } 333 | 334 | div.conference h5, 335 | div.apero h5{ 336 | background-color: #94a1c5; 337 | color: #fff; 338 | padding-left: 1ex; 339 | padding-right: 1ex; 340 | } 341 | 342 | .vmiddle { 343 | vertical-align: middle !important; 344 | } 345 | 346 | --------------------------------------------------------------------------------