Skip to content

Commit

Permalink
Provide a crop plan CSV export.
Browse files Browse the repository at this point in the history
  • Loading branch information
mstenta committed Mar 14, 2024
1 parent cf6d460 commit 49ee70b
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 4 deletions.
1 change: 1 addition & 0 deletions farm_crop_plan.info.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type: module
package: farmOS Contrib
core_version_requirement: ^10
dependencies:
- csv_serialization:csv_serialization
- farm_entity
- farm_import_csv
- farm_log
Expand Down
2 changes: 1 addition & 1 deletion farm_crop_plan.links.action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ farm_crop_plan.add_planting:
- entity.plan.canonical
farm_crop_plan.import:
route_name: farm_crop_plan.import
title: Import
title: Import/export
appears_on:
- entity.plan.canonical
15 changes: 14 additions & 1 deletion farm_crop_plan.routing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,17 @@ farm_crop_plan.import:
plan:
type: entity:plan
bundle:
- crop
- crop
farm_crop_plan.export:
path: /plan/{plan}/export
defaults:
_controller: \Drupal\farm_crop_plan\Controller\CropPlanImport::exporter
migration_id: crop_plan_records
requirements:
_entity_access: plan.view
options:
parameters:
plan:
type: entity:plan
bundle:
- crop
192 changes: 190 additions & 2 deletions src/Controller/CropPlanImport.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,216 @@

namespace Drupal\farm_crop_plan\Controller;

use Drupal\Core\Database\Connection;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Link;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\farm_crop_plan\CropPlanInterface;
use Drupal\farm_import_csv\Access\CsvImportMigrationAccess;
use Drupal\farm_import_csv\Controller\CsvImportController;
use Drupal\farm_location\LogLocationInterface;
use Drupal\migrate\Plugin\MigrationPluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\SerializerInterface;

/**
* Crop plan import controller.
*/
class CropPlanImport extends CsvImportController {
class CropPlanImport extends CsvImportController implements ContainerInjectionInterface {

/**
* The crop plan service.
*
* @var \Drupal\farm_crop_plan\CropPlanInterface
*/
protected $cropPlan;

/**
* Log location service.
*
* @var \Drupal\farm_location\LogLocationInterface
*/
protected $logLocation;

/**
* The serializer service.
*
* @var \Symfony\Component\Serializer\SerializerInterface
*/
protected $serializer;

/**
* Constructs a new CropPlanImport.
*
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_link_tree
* The menu link tree service.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
* @param \Drupal\migrate\Plugin\MigrationPluginManager $plugin_manager_migration
* The migration plugin manager.
* @param \Drupal\farm_import_csv\Access\CsvImportMigrationAccess $migration_access
* The CSV import migration access service.
* @param \Drupal\Core\Database\Connection $database
* The database connection.
* @param \Drupal\farm_crop_plan\CropPlanInterface $crop_plan
* The crop plan service.
* @param \Drupal\farm_location\LogLocationInterface $log_location
* Log location service.
* @param \Symfony\Component\Serializer\SerializerInterface $serializer
* The serializer service.
*/
public function __construct(MenuLinkTreeInterface $menu_link_tree, FormBuilderInterface $form_builder, MigrationPluginManager $plugin_manager_migration, CsvImportMigrationAccess $migration_access, Connection $database, CropPlanInterface $crop_plan, LogLocationInterface $log_location, SerializerInterface $serializer) {
parent::__construct($menu_link_tree, $form_builder, $plugin_manager_migration, $migration_access, $database);
$this->cropPlan = $crop_plan;
$this->logLocation = $log_location;
$this->serializer = $serializer;
}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('menu.link_tree'),
$container->get('form_builder'),
$container->get('plugin.manager.migration'),
$container->get('farm_import_csv.access'),
$container->get('database'),
$container->get('farm_crop_plan'),
$container->get('log.location'),
$container->get('serializer'),
);
}

/**
* {@inheritdoc}
*/
public function importer(string $migration_id): array {
public function importer(string $migration_id, $plan = NULL): array {
$build = parent::importer($migration_id);

// Remove the template download link.
if (!empty($build['columns']['template'])) {
unset($build['columns']['template']);
}

// Set the weight of the columns fieldset.
$build['columns']['#weight'] = -100;

// Add a link to download a CSV export of the plan.
if (!empty($plan)) {
$build['export'] = [
'#type' => 'details',
'#title' => $this->t('Export plan'),
'#description' => $this->t('Use this to download a CSV of the plan which can be edited and re-imported.'),
'#open' => TRUE,
'#weight' => -50,
];
$export_link = Link::createFromRoute($this->t('Download CSV'), 'farm_crop_plan.export', ['plan' => $plan->id()]);
$build['export']['link'] = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $export_link->toString(),
];
}

// Allow updating.
$build['form']['update_existing_records']['#value'] = TRUE;

return $build;
}

/**
* Download a CSV representation of the plan.
*
* @param string $migration_id
* The migration ID.
* @param \Drupal\plan\Entity\PlanInterface $plan
* The crop plan entity.
*
* @return \Symfony\Component\HttpFoundation\Response
* An application/csv file download response object.
*/
public function exporter(string $migration_id, $plan) {

// Draft an application/csv response.
$filename = 'crop-plan-' . $plan->id() . '.csv';
$response = new Response();
$response->headers->set('Content-Type', 'application/csv');
$response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"');

// Load expected column names.
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = $this->pluginManagerMigration->getDefinition($migration_id);
$column_names = array_filter(array_column($migration['third_party_settings']['farm_import_csv']['columns'] ?? [], 'name'));

// Load crop_planting records for the plan.
$crop_plantings = $this->cropPlan->getCropPlantings($plan);

// If there are no crop_planting records, add a single template row.
$row_template = array_fill_keys($column_names, '');
$row_template['plan_id'] = $plan->id();
$data = [];
if (empty($crop_plantings)) {
$data[] = $row_template;
}

// Iterate through the crop_planting records and build data rows.
foreach ($crop_plantings as $crop_planting) {
$row = $row_template;

// Set the plant asset ID.
$row['plant_id'] = $crop_planting->getPlant()->id();

// Load plant type labels.
$plant_types = array_map(function ($term) {
return $term->label(0);
}, $crop_planting->getPlant()->get('plant_type')->referencedEntities());
$row['plant_type'] = implode(', ', $plant_types);

// Look up the first seeding log location and timestamp.
$log = $this->cropPlan->getFirstLog($crop_planting, 'seeding');
if (!empty($log)) {
$locations = array_map(function ($asset) {
return $asset->label(0);
}, $this->logLocation->getLocation($log));
$row['seeding_location'] = implode(', ', $locations);
$row['seeding_date'] = date('c', $log->get('timestamp')->value);
}

// Look up the first transplanting log location and timestamp.
$log = $this->cropPlan->getFirstLog($crop_planting, 'transplanting');
if (!empty($log)) {
$locations = array_map(function ($asset) {
return $asset->label(0);
}, $this->logLocation->getLocation($log));
$row['transplanting_location'] = implode(', ', $locations);
$row['transplanting_date'] = date('c', $log->get('timestamp')->value);
}

// Add transplant_days, if available.
if (!empty($crop_planting->get('transplant_days')->value)) {
$row['transplant_days'] = $crop_planting->get('transplant_days')->value;
}

// Add maturity_days, if available.
if (!empty($crop_planting->get('maturity_days')->value)) {
$row['maturity_days'] = $crop_planting->get('maturity_days')->value;
}

// Add harvest_days, if available.
if (!empty($crop_planting->get('harvest_days')->value)) {
$row['harvest_days'] = $crop_planting->get('harvest_days')->value;
}

// Add the row data.
$data[] = $row;
}

// Serialize and return the data.
$response->setContent($this->serializer->serialize($data, 'csv'));
return $response;
}

}
4 changes: 4 additions & 0 deletions tests/files/export-crop-plan.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
plan_id,plant_id,plant_type,seeding_date,seeding_location,transplanting_date,transplanting_location,transplant_days,maturity_days,harvest_days
1,1,Corn,2024-06-01T00:00:00+10:00,"Field A",2024-07-01T00:00:00+10:00,"Field A",30,60,7
1,2,Beans,2024-07-01T00:00:00+10:00,"Field A",2024-08-01T00:00:00+10:00,"Field A",30,60,7
1,3,Squash,2024-08-01T00:00:00+10:00,"Field A",2024-09-01T00:00:00+10:00,"Field A",30,60,7
48 changes: 48 additions & 0 deletions tests/src/Functional/CropPlanTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Drupal\Tests\farm_crop_plan\Functional;

use Drupal\Tests\farm_crop_plan\Traits\MockCropPlanEntitiesTrait;
use Drupal\Tests\farm_test\Functional\FarmBrowserTestBase;

/**
* Tests for farmOS crop plan.
*
* @group farm_crop_plan
*/
class CropPlanTest extends FarmBrowserTestBase {

use MockCropPlanEntitiesTrait;

/**
* {@inheritdoc}
*/
protected static $modules = [
'farm_crop_plan',
'farm_land',
'farm_seeding',
'farm_transplanting',
];

/**
* Test crop plan export.
*/
public function testCropPlanExport() {

// Create mock plan entities.
$this->createMockPlanEntities();

// Create and login a test user with access to view crop plans and logs.
$user = $this->createUser(['view any crop plan', 'view any log']);
$this->drupalLogin($user);

// Test that the plan export contains expected headers and content.
$output = $this->drupalGet('plan/1/export');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->responseHeaderContains('Content-Type', 'application/csv');
$this->assertSession()->responseHeaderContains('Content-Disposition', 'attachment; filename="crop-plan-1.csv');
$expected = file_get_contents(__DIR__ . '/../../files/export-crop-plan.csv');
$this->assertEquals($expected, $output);
}

}

0 comments on commit 49ee70b

Please sign in to comment.