Edit CSV in Panel?

We have a CSV file that includes courses / seminars.
At the moment we read these via Kirby CSV Handler (fopen) and place the issues in sortable form at the frontend.
Now the user should be able to define courses as “booked”.
Is there a way to edit the csv inside the panel (via blueprint and structure field) without importing the csv an convert these to txt.
How would your solution look like?

You can do that, but not in the normal Panel pages section but as a separate Panel page. For examples of creating a separate Panel page, have a look at the Panel Media or Kirby Logger

For an example of editing content in such a separate Panel view, see Kirby translations or Code Editor.

I had this requirement two times in the past, but in German language, one on a online Kirby website at two timetables, e.g. here (is online) and a development version of a website (a menuetable of a restaurant).

But I decided to use “Kirby as a database” (in it’s file system) for the future and have converted the csv-file each line to a page in the content of Kirby during the development (I used Libre Office for the convert using a Form Letter (German word: Serienbrief) in form of a file for each line of the csv-file afterwards). After this every line of that table is a new page. Nobody needs to import a csv-file any more.

I added a blueprint for every new page (former line), the name of the new pages content files follow this blueprint file name …

BTW: I once created a plugin that generates pages or structure field entries from a csv file (if there is no reason to keep the csv file).

1 Like


Yes I know, but my website goes online at 2015-02-07.

And to use of a webpage in the Kirby content for an item has the advantage over items of a structure field, that pages can be set online or offline within the panel without deleting its content.
And such item pages can be hidden, it is not necessary, that such pages are visual pages.

And - of course - such item pages can be stored at serval places, e.g. within a “admin” section of pages, outside the tree of normal webpages.

Well, yes, but my answer was intended for the TO. And he might even have a reason to stick with his csv file, whatever that is.

Well, yes, but you can give structure entries a status field as well, so it really depends on what you prefer and on the content of these entries. Pages don’t really make sense for an address list or calendar entries. But anyway, the question was how to edit a CSV file in the Panel, and not how to create pages from the file. So lets wait for the TO.

Sorry, I am a fan to use “Kirby as a database” (in it’s file system). So I’m not able to agree to …

But this may depend on the individual education of every developer.

And I like Kirby very much:
Kirby is open for such a solution…:grinning::grinning::grinning::grin:

@texnixe Thank You!
We have now made via your Solution with the Kirby Translation as Template.

This is working!

Our Steps were those:

  1. We made an Folder in the site > plugins > seminare
  2. There we create a php File named seminare.php
  3. In this Folder we have two Folders
    site > plugins > seminare > views
    site > plugins > seminare > widget
  4. In the widget Folder is the widget.php
// seminare/widget/widget.php
return array(
    'title' => array(
        'text'       => 'Seminare CSV Datei bearbeiten',
        'compressed' => true
    'options' => array(
            'text' => 'CSV bearbeiten',
            'icon' => 'pencil',
            'link' => 'seminare'
    'html'  => function () {
        return 'Hier können Sie die Seminare bearbeiten <br />
        Bsp. Ausgebucht und HTML-Text.<br /><br />
        <strong>Links:</strong><br />
        Extrerne Links mit http://www.adresse.de<br />
        (link: http://www.google.de text: Hier zur Suche gehen)<br />
        Interne Links:<br />
        (link: /pfadzur/seite text: Zur Seite gehn)<br />
        <br />
        <strong>Bilder:</strong><br />
        Extrerne Bilder mit http://www.adresse.de<br />
        (image: http://www.site.de/bild.png)<br />';

  1. Create index.php in site > plugins > seminare > views
<div class="bars cf">
	<div class="">
		<div class="section">
			<h2 class="hgroup hgroup-single-line hgroup-compressed cf"><span class="hgroup-title">Seminare</span></h2>
			<form method="POST" action="<?php echo panel()->urls()->index . '/seminare' ?>" class="form">
				<table style="width: 100%">
						<td style="font-style: italic; border-bottom: 2px solid grey">Kursnummer</td>
						<td style="font-style: italic; border-bottom: 2px solid grey">Headline</td>
						<td style="font-style: italic; border-bottom: 2px solid grey">Datum von</td>
						<td style="font-style: italic; border-bottom: 2px solid grey">Datum bis</td>
						<td style="font-style: italic; border-bottom: 2px solid grey">Web_Link_Info_Referent</td>
						<td style="font-style: italic; border-bottom: 2px solid grey">Ausgebucht</td>
<?php foreach ($seminare as $key => $seminar) { ?>
						<td style="font-style: italic; border-bottom: 1px solid grey"><?php echo esc($seminar['kursnummer']); ?></td>
						<td style="font-style: italic; border-bottom: 1px solid grey"><?php echo esc($seminar['headline']); ?></td>
						<td style="font-style: italic; border-bottom: 1px solid grey"><?php echo esc($seminar['datum_von']); ?></td>
						<td style="font-style: italic; border-bottom: 1px solid grey"><?php echo esc($seminar['datum_bis']); ?></td>
						<td style="font-style: italic; border-bottom: 1px solid grey">
							<textarea name="seminar__web_link_info_referent__<?php echo $seminar['id']; ?>"><?php echo esc($seminar['web_link_info_referent']); ?></textarea>
						<td style="font-style: italic; border-bottom: 1px solid grey">
							<input name="seminar__ausgebucht__<?php echo $seminar['id']; ?>" type="checkbox" value="x"<?php if (strtoupper($seminar['ausgebucht']) == 'X') { echo " checked"; } ?> />
							<?php echo esc($seminar['ausgebucht']); ?>
<?php } ?>
				<input type="hidden" name="csrf" value="<?php echo panel()->csrf() ?>">
				<fieldset class="fieldset buttons buttons-centered">
					<input class="btn btn-rounded btn-submit" type="submit" value="<?php echo l::get('save'); ?>">
  1. Create seminare.php index.php in site > plugins > seminare

namespace Seminare;
use \Kirby\Panel\Topbar;
use \l;
use \r;

if(class_exists('Panel')) {

	class SeminareController extends \Kirby\Panel\Controllers\Base {

		public function view($file, $data = array()) {
			return new SeminareView($file, $data);

		public function getSeminare() {
			$csv_filename = site()->events_csv_file()->value();
			$csv_path = site()->root();
			$seminare = array();
			$rows_count = 0;
			if (($csv_handle = fopen($csv_path . DS . $csv_filename, "r")) !== FALSE) {
				while (($row = fgetcsv($csv_handle, 1000, ",")) !== FALSE) {
					if ($rows_count > 0) {
						$seminare[] = array();
						$seminare[count($seminare) - 1]['id'] = $row[0];
						$seminare[count($seminare) - 1]['hauptkategorie'] = $row[1];
						$seminare[count($seminare) - 1]['unterkategorie'] = $row[2];
						$seminare[count($seminare) - 1]['subheadline_unterkategorie'] = $row[3];
						$seminare[count($seminare) - 1]['einleitungstext'] = $row[4];
						$seminare[count($seminare) - 1]['kursnummer'] = $row[5];
						$seminare[count($seminare) - 1]['datum_von'] = $row[6];
						$seminare[count($seminare) - 1]['datum_bis'] = $row[7];
						$seminare[count($seminare) - 1]['referenten'] = $row[8];
						$seminare[count($seminare) - 1]['chronologie_headline'] = $row[9];
						$seminare[count($seminare) - 1]['headline'] = $row[10];
						$seminare[count($seminare) - 1]['subheadline'] = $row[11];
						$seminare[count($seminare) - 1]['text'] = $row[12];
						$seminare[count($seminare) - 1]['beginn_ende'] = $row[13];
						$seminare[count($seminare) - 1]['text_sonstiges'] = $row[14];
						$seminare[count($seminare) - 1]['preis_einleitung'] = $row[15];
						$seminare[count($seminare) - 1]['tb'] = $row[16];
						$seminare[count($seminare) - 1]['vp'] = $row[17];
						$seminare[count($seminare) - 1]['preis_anmerkung'] = $row[18];
						$seminare[count($seminare) - 1]['besonderes_angebot'] = $row[19];
						$seminare[count($seminare) - 1]['besonderes_angebot_sparen'] = $row[20];
						$seminare[count($seminare) - 1]['besonderes_angebot_sparen'] = $row[20];
						$seminare[count($seminare) - 1]['web_link_info_referent'] = $row[21];
						$seminare[count($seminare) - 1]['ausgebucht'] = $row[22];
			return compact('seminare');

		public function setSeminare() {			
			$csv_filename = site()->events_csv_file()->value();
			$csv_path = site()->root();

			$seminare = $this->getSeminare();
			$seminare = $seminare['seminare'];

			foreach ($seminare as $key => $seminar) {
				if (@$_REQUEST['seminar__web_link_info_referent__' . $seminar['id']]) {
					$seminare[$key]['web_link_info_referent'] = $_REQUEST['seminar__web_link_info_referent__' . $seminar['id']];
				} else {
					$seminare[$key]['web_link_info_referent'] = "";
				if (@$_REQUEST['seminar__ausgebucht__' . $seminar['id']]) {
					$seminare[$key]['ausgebucht'] = "x";
				} else {
					$seminare[$key]['ausgebucht'] = "";

			array_unshift($seminare, array("id","Hauptkategorie","Unterkategorie","Subheadline_Unterkategorie","Einleitungstext","Kursnummer","Datum_von","Datum_bis","Referenten","Cronologie_Headline","Headline","Subheadline","Text","Beginn_Ende","Text_Sonstiges","Preis_Einleitung","TB","VP","Preis_Anmerkung","Besonderes_Angebot","Besonderes_Angebot_Sparen","Web_Link_Info_Referent","Ausgebucht"));

			$csv_handle = fopen($csv_path . DS . basename($csv_filename, '.csv') . '_new.csv', 'a');
			foreach ($seminare as $seminar) {
	    		fputcsv($csv_handle, $seminar, ",", '"');

    		rename($csv_path . DS . $csv_filename, $csv_path . DS . basename($csv_filename, '.csv') . '_old.csv');
    		rename($csv_path . DS . basename($csv_filename, '.csv') . '_new.csv', $csv_path . DS . $csv_filename);

		public function index() {
			if(r::is('post')) {
			return $this->screen('index', new TopbarGenerator(), $this->getSeminare());

	class TopbarGenerator {
		public function topbar(Topbar $topbar) {
			$topbar->append(panel()->site()->url() . '/panel/seminare', 'Seminare');

	class SeminareView extends \Kirby\Panel\View {
		public function __construct($file, $data = array()) {
			parent::__construct($file, $data);
			$this->_root = __DIR__ . DS . 'views';

	$panel = panel();

	$panel->routes[] = [
		'pattern' => 'seminare',
		'action'  => function() {
			$ctrl = new SeminareController();
			return $ctrl->index();
		'method'  => 'GET|POST',
		'filter'  => array('auth')

	$kirby->set('widget', 'widget', __DIR__ . '/widget');