Commit 3250470a authored by Sean Madsen's avatar Sean Madsen Committed by GitHub

Merge pull request #45 from seanmadsen/refactor-model

Refactor model
parents 6bea8e5b 1ab4d257
......@@ -13,9 +13,11 @@
!var/sessions/.gitkeep
!var/SymfonyRequirements.php
/vendor/
/web/bundles/
/web/static/*
!/web/static/css
#not sure how else to ensure that the following two files aren't deployed on prod
/web/config.php
/web/*
!/web/.htaccess
!/web/app.php
!/web/app_dev.php
!/web/robots.txt
!/web/static
......@@ -2,6 +2,6 @@
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
class AppCache extends HttpCache
{
class AppCache extends HttpCache {
}
......@@ -3,68 +3,60 @@
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(),
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
new AppBundle\AppBundle(),
];
class AppKernel extends Kernel {
public function registerBundles() {
$bundles = [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(),
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
new AppBundle\AppBundle(),
];
if (in_array($this->getEnvironment(), ['dev', 'test'], TRUE)) {
$bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
$bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
}
if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
$bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
$bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
}
return $bundles;
}
return $bundles;
}
public function getRootDir() {
return __DIR__;
}
public function getRootDir()
{
return __DIR__;
}
public function getCacheDir() {
return dirname(__DIR__) . '/var/cache/' . $this->getEnvironment();
}
public function getCacheDir()
{
return dirname(__DIR__).'/var/cache/'.$this->getEnvironment();
}
public function getLogDir() {
return dirname(__DIR__) . '/var/logs';
}
public function getLogDir()
{
return dirname(__DIR__).'/var/logs';
}
public function registerContainerConfiguration(LoaderInterface $loader) {
$loader->load($this->getRootDir() . '/config/config_' . $this->getEnvironment() . '.yml');
}
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');
protected function buildContainer() {
$container = parent::buildContainer();
if ($container->hasParameter('mkdocs_path')) {
$_ENV['PATH'] = $container->getParameter('mkdocs_path')
. PATH_SEPARATOR . getenv('PATH');
putenv("PATH=" . $_ENV['PATH']);
}
protected function buildContainer()
{
$container = parent::buildContainer();
if ($container->hasParameter('mkdocs_path')) {
$_ENV['PATH'] = $container->getParameter('mkdocs_path')
. PATH_SEPARATOR . getenv('PATH');
putenv("PATH=" . $_ENV['PATH']);
}
if (!$container->hasParameter('publisher_repos_dir')) {
// This isn't really a good place to put it because it gets deleted
// whenever you clear the cache.
$container->setParameter('publisher_repos_dir',
$container->getParameter('kernel.cache_dir') . '/repos'
);
}
return $container;
if (!$container->hasParameter('publisher_repos_dir')) {
// This isn't really a good place to put it because it gets deleted
// whenever you clear the cache.
$container->setParameter('publisher_repos_dir', $container->getParameter('kernel.cache_dir') . '/repos'
);
}
return $container;
}
}
<p><strong>Note from <a href="{{ app.request.getUriForPath('/') }}">{{ app.request.getUriForPath('/') }}</a>:<br />Published '{{ branch }}' branch of '
{{ book }}' in '{{ lang }}'.</strong>
</p>
<p>Please check the publish log for any errors:</p>
<ul>
{% for message in messages %}
<li>
<strong>{{ message.label }}:</strong>
{{ message.content|raw }}
</li>
{% endfor %}
</ul>
......@@ -5,7 +5,8 @@
<meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
<title>{% block title %}CiviCRM documentation{% endblock %}</title>
<link rel="stylesheet" href="{{ asset('css/publish.css') }}" /> {% block stylesheets %}{% endblock %}
<link rel="stylesheet" href="{{ asset('/static/css/main.css') }}" />
{% block stylesheets %}{% endblock %}
<link media="all" href="https://civicrm.org/sites/all/themes/civicrm_bootstrap/css/style.css" rel="stylesheet" type="text/css">
</head>
......
......@@ -6,7 +6,7 @@ use Composer\Autoload\ClassLoader;
/**
* @var ClassLoader $loader
*/
$loader = require __DIR__.'/../vendor/autoload.php';
$loader = require __DIR__ . '/../vendor/autoload.php';
AnnotationRegistry::registerLoader([$loader, 'loadClass']);
......
server {
server_name docs;
root /var/www/civicrm-docs/web/static;
root /var/www/civicrm-docs/web;
location ~ ^/(admin|[[:alpha:]]+/*$|$) {
# try to serve file directly, fallback to app.php
try_files $uri /app.php$is_args$args;
# For a development configuration, change "app" to "app_dev" everywhere
# below. Then you'll see the Symfony web debug toolbar when viewing pages.
# Requests for static book files
# - Book slug can't begin with underscore
# - Book slug can't be "admin"
# - Must begin with three distinct path pieces, separated by forward slashes
location ~ ^/(?!_)(?!admin/)[^/]+/[^/]+/[^/]+ {
# - Use a trailing slash for the $uri to treat paths like directories
# - Then, try to find directories that match our request.
# - If we can't find anything, we send the request to the app and let
# it deal with the rest.
try_files $uri/ /app.php$is_args$args;
}
# requests for Symfony app functionality
location / {
# - Don't use a trailing slash for $uri here. This line is straight from
# the Symfony recommended nginx settings
try_files $uri /app.php$is_args$args;
}
# PROD
location ~ ^/app\.php(/|$) {
alias /var/www/civicrm-docs/web/;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
......@@ -19,11 +35,21 @@ server {
# Otherwise, PHP's OPcache may not properly detect changes to
# your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126
# for more information).
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
# Prevents URIs that include the front controller. This will 404:
# http://domain.tld/app.php/some-path
# Remove the internal directive to allow URIs like this
internal;
}
# return 404 for all other php files not matching the front controller
# this prevents access to other php files you don't want to be accessible.
location ~ \.php$ {
return 404;
}
#error_log /var/log/nginx/civicrm-docs-error.log;
#access_log /var/log/nginx/civicrm-docs-access.log;
}
......@@ -4,18 +4,32 @@ parameters:
# parameter_name: value
services:
book.loader:
class: AppBundle\Utils\BookLoader
arguments: ['%kernel.root_dir%/config/books']
github.hook.processor:
class: AppBundle\Utils\GitHubHookProcessor
arguments: ['@publisher', '@book.loader']
publisher:
class: AppBundle\Utils\Publisher
arguments: ['@request_stack', '@publish.logger', '@filesystem', %kernel.root_dir%/config/books, %publisher_repos_dir%, %kernel.root_dir%/../web/static]
publish.logger:
class: Monolog\Logger
arguments: ['publish', ['@streamhandler']]
streamhandler:
class: Monolog\Handler\StreamHandler
arguments: [%kernel.logs_dir%/publish.log]
library:
class: AppBundle\Model\Library
arguments:
- %kernel.root_dir%/../books
github.hook.processor:
class: AppBundle\Utils\GitHubHookProcessor
publisher:
class: AppBundle\Utils\Publisher
arguments:
- '@request_stack'
- '@publish.logger'
- '@filesystem'
- '@library'
- %publisher_repos_dir%
- %kernel.root_dir%/../web
publish.logger:
class: Monolog\Logger
arguments:
- 'publish'
- ['@streamhandler']
streamhandler:
class: Monolog\Handler\StreamHandler
arguments:
- %kernel.logs_dir%/publish.log
......@@ -4,6 +4,5 @@ namespace AppBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
class AppBundle extends Bundle {
}
......@@ -9,23 +9,19 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class DocsListCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
->setName('docs:list')
->setDescription('List available books');
}
class DocsListCommand extends ContainerAwareCommand {
protected function execute(InputInterface $input, OutputInterface $output)
{
/** @var \AppBundle\BookLoader $books */
$books = $this->getContainer()->get('book.loader');
$table = new Table($output);
$table->setHeaders(array('book', 'lang', 'repo', 'branch'));
$table->addRows($books->findAsList());
$table->render();
}
protected function configure() {
$this->setName('docs:list')->setDescription('List available books');
}
protected function execute(InputInterface $input, OutputInterface $output) {
/** @var \AppBundle\Model\Library $library */
$library = $this->getContainer()->get('library');
$table = new Table($output);
$table->setHeaders(array('book', 'language', 'repo', 'branch'));
$table->addRows($library->booksAsTable());
$table->render();
}
}
......@@ -8,47 +8,37 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class DocsPublishCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
->setName('docs:publish')
->setDescription('...')
->addArgument('paths', InputArgument::IS_ARRAY,
'One or more book expressions ("book/lang/branch"). (Default: all)');
class DocsPublishCommand extends ContainerAwareCommand {
protected function configure() {
$this
->setName('docs:publish')
->setDescription('Publish one or more books')
->addArgument(
'identifiers',
InputArgument::IS_ARRAY,
'One or more book identifiers (e.g. "user/en/master"). Partial '
. 'identifiers are acceptable (e.g. "user/en" will publish all '
. 'English versions of the User Guide. If no identifiers are '
. 'specified, then all versions of all languages in all books will '
. 'be published.');
}
protected function execute(InputInterface $input, OutputInterface $output) {
/** @var \AppBundle\Utils\Publisher $publisher */
$publisher = $this->getContainer()->get('publisher');
$identifiers = $input->getArgument('identifiers');
if ($identifiers) {
foreach ($identifiers as $identifier) {
$publisher->publish($identifier);
}
}
protected function execute(InputInterface $input, OutputInterface $output)
{
/** @var \AppBundle\BookLoader $books */
$books = $this->getContainer()->get('book.loader');
/** @var \AppBundle\Utils\Publisher $publisher */
$publisher = $this->getContainer()->get('publisher');
$rows = $books->findAsList();
if (empty($rows)) {
$output->writeln("<error>No books found</error>");
}
$rowKeys = $input->getArgument('paths') ? $input->getArgument('paths') : array_keys($rows);
foreach ($rowKeys as $rowKey) {
$row = $rows[$rowKey];
$output->writeln("");
$output->writeln(sprintf("Publish [%s/%s/%s] (from %s)",
$row['book'], $row['lang'], $row['branch'], $row['repo']));
$publisher->publish($row['book'], $row['lang'], $row['branch']);
foreach ($publisher->getMessages() as $message) {
$output->writeln($message['label'] . ': ' . $message['content']);
}
$publisher->clearMessages();
}
else {
$publisher->publish();
}
foreach ($publisher->getMessages() as $message) {
$output->writeln($message['label'] . ': ' . $message['content']);
}
}
}
<?php
namespace AppBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class DocsServeCommand
* @package AppBundle\Command
*
* This is a dumb wrapper around `mkdocs serve`. It basically just cd's into the
* appropriate folder and then passes-through the args.
*/
class DocsServeCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
->setName('docs:serve')
->setDescription('Run the builtin docs development server')
->addOption('dev-addr', 'a', InputOption::VALUE_OPTIONAL,
'IP address and port to serve documentation')
->addOption('strict', 's', InputOption::VALUE_NONE,
'Enable strict mode. This will cause MkDocs to abort the build on any warnings.')
->addOption('theme', 't', InputOption::VALUE_OPTIONAL,
'The theme to use when building your documentation [cosmo|cyborg|readthedocs|yeti|journal|bootstrap|readable|united|simplex|flatly|spacelab|amelia|cerulean|slate|mkdocs')
->addOption('livereload', null, InputOption::VALUE_NONE,
'Enable the live reloading in the development server.')
->addOption('no-livereload', null, InputOption::VALUE_NONE,
'Enable the live reloading in the development server.')
->addOption('quiet', 'q', InputOption::VALUE_NONE,
'Silence warnings')// ->addOption('verbose', 'v', InputOption::VALUE_NONE, 'Enable verbose output')
->addArgument('[book/lang/[branch]]', InputArgument::REQUIRED,
'A book expression (e.g. "dev/en/master" or "user/fr/4.6"). The [book] and [lang] are mandatory. If [branch] is specified, it will be checked out.')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
if (preg_match(';^(\w+)/(\w+)?$;', $input->getArgument('[book/lang/[branch]]'), $matches)) {
$book = $matches[1];
$lang = $matches[2];
$branch = NULL;
} elseif (preg_match(';^(\w+)/(\w+)/(\w+)$;', $input->getArgument('[book/lang/[branch]]'), $matches)) {
$book = $matches[1];
$lang = $matches[2];
$branch = $matches[3];
} else {
$output->writeln("<error>Malformed [book/lang/branch]</error>");
return 1;
}
$repoDir = $this->getContainer()->getParameter('publisher_repos_dir') . "/$book/$lang";
if (file_exists($repoDir)) {
$output->writeln("<info>Found git repo for $book/$lang in \"$repoDir\"</info>");
} else {
$output->writeln("<error>Failed to find git repo for $book/$lang in \"$repoDir\".</error>");
$output->writeln("<error>Perhaps you should run \"docs:publish\" first?</error>");
return 1;
}
$command = 'mkdocs serve';
foreach (array('dev-addr', 'theme') as $option) {
if ($input->getOption($option)) {
$command .= " --{$option} " . escapeshellarg($input->getOption($option));
}
}
foreach (array('strict', 'livereload', 'no-livereload', 'quiet') as $option) {
if ($input->getOption($option) ) {
$command .= " --{$option}";
}
}
if ($branch) {
$output->writeln("<info>Check out branch \"$branch\"</info>");
self::passthruOK($repoDir, "git checkout $branch");
}
$output->writeln("<info>Launch \"$command\"</info>");
self::passthruOK($repoDir, $command);
}
protected function passthruOK($newcwd, $command) {
$oldcwd = getcwd();
chdir($newcwd);
passthru($command, $return);
chdir($oldcwd);
if ($return) {
throw new \RuntimeException("Received error from command ($command)");
}
}
}
<?php
//
// namespace AppBundle\Controller;
//
// use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
// use Symfony\Bundle\FrameworkBundle\Controller\Controller;
// use Symfony\Component\HttpFoundation\Request;
//
// class DefaultController extends Controller
// {
// /**
// * @Route("/", name="homepage")
// */
// public function indexAction(Request $request)
// {
// // replace this example code with whatever you need
// return $this->render('default/index.html.twig', [
// 'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..'),
// ]);
// }
// }
......@@ -6,79 +6,105 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Yaml\Parser;
use AppBundle\Model\Library;
//@TODO Make the errors that will occur if the slugs don't pass the regexp more obvious to the user
class PublishController extends Controller {
class PublishController extends Controller
{
/**
* @Route("/admin/publish")
*/
public function PublishInstructionsAction()
{
return $this->render('AppBundle:Publish:publish.html.twig');
/**
*
* @var \AppBundle\Utils\Publisher
*/
private $publisher;
/**
*
* @var bool TRUE if the book was published without any errors
*/
private $publishSuccess;
/**
* @Route("/admin/publish{identifier}" , requirements={"identifier": ".*"})
*/
public function PublishAction(Request $request, $identifier) {
$this->publisher = $this->get('publisher');
$bookSlug = Library::parseIdentifier($identifier)['bookSlug'];
if ($bookSlug) {
$this->publishSuccess = $this->publisher->publish($identifier);
}
/**
* @Route("/admin/publish/{book}/{lang}/{branch}", requirements={
* "lang":"[[:alpha:]]{2}",
* "book":"[[:alpha:]\-]+",
* "branch":"[[:alnum:]\-\.]+",
* })
*/
public function PublishAction(Request $request, $lang, $book, $branch)
{
$publisher = $this->get('publisher');
$publisher->publish($book, $lang, $branch);
$content['messages'] = $publisher->getMessages();
return $this->render('AppBundle:Publish:publish.html.twig', $content);
else {
$this->publisher->addMessage('INFO', "Publish action called without a book "
. "specified, thus attempting to publish all books.");
$this->publisher->addMessage('CRITICAL', "Publishing all books it not "
. "supported through the web interface because it has the potential "
. "to really slow down the server. If you want to publish all books "
. "you can run 'docs:publish' from the command line interface.");
}
/**
* @Route("/admin/listen")
*/
public function ListenAction(Request $request)
{
$body = $request->getContent();
$event = $request->headers->get('X-GitHub-Event');
$payload = json_decode($body);
$books = $this->get('book.loader')->find();
$content['messages'] = $this->publisher->getMessages();
return $this->render('AppBundle:Publish:publish.html.twig', $content);
}
$processor = $this->get('github.hook.processor');
$processor->process($event, $payload);
if(!$processor->published){
return new Response('Something went wrong during publishing.', 200); // @TODO Add more appropriate error code
/**
* @Route("/admin/listen")
*/
public function ListenAction(Request $request) {
$body = $request->getContent();
$event = $request->headers->get('X-GitHub-Event');
$processor = $this->get('github.hook.processor');
try {
$processor->process($event, json_decode($body));
}
catch (\Exception $e) {
$response = "CRITICAL - Skipping the publishing process due to the "
. "following reason: " . $e->getMessage();
return new Response($response, 200);
}
$library = $this->get('library');
$identifiers = $library->getIdentifiersByRepo($processor->repo);
if ($identifiers) {
$this->publisher = $this->get('publisher');
foreach ($identifiers as $identifier) {
$this->publisher->publish("{$identifier}/{$processor->branch}");
if ($this->publisher->version) {
$this->sendEmail($processor->recipients);
}
$messages = $processor->getMessages();
$subject = $processor->getSubject();
$recipients = $processor->getRecipients();
$mail = \Swift_Message::newInstance()
->setSubject("[CiviCRM docs] $subject")
->setFrom('no-reply@civicrm.org')
}
$response = $this->publisher->getMessagesInPlainText();
}
else {
$response = "CRITICAL - No books found which match {$processor->repo}";
}
return new Response($response, 200);
}
/**
* Send notification emails after publishing
*
* @param array $extraRecipients Array of strings for email addresses that
* should receive the notification email. If
* non are specified, then the email will be
* sent to all addresses set in the book's yaml
* configuration.
*/
private function sendEmail($extraRecipients = array()) {
$subject = $this->publisher->status;
$recipients = array_unique(array_merge(
$extraRecipients,
$this->publisher->language->watchers));
$mail = \Swift_Message::newInstance()
->setSubject("$subject")
->setFrom('no-reply@civicrm.org', "CiviCRM docs")
->setTo($recipients)
->setBody(
$this->renderView(
'Emails/notify.html.twig', array(
'branch' => $processor->publisher->branch,
'book' => $processor->publisher->book,
'lang' => $processor->publisher->lang,
'messages' => $processor->publisher->getMessages()
$this->renderView('AppBundle:Emails:notify.html.twig',
array(
'publishURLBase' => $this->publisher->publishURLBase,
'status' => $this->publisher->status,
'messages' => $this->publisher->messages,
)
),
'text/html'
), 'text/html'
);
$this->get('mailer')->send($mail);
return new Response($subject, 200);
$this->get('mailer')->send($mail);
}
}
}
......@@ -4,64 +4,17 @@ namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Intl\ResourceBundle\RegionBundle