mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 01:32:38 +00:00
Added possibility to export the calendar in iCal format (closes #141)
This commit is contained in:
parent
77b0bc675c
commit
90291fdbca
@ -4,7 +4,8 @@
|
||||
"slim/slim": "^3.8",
|
||||
"morris/lessql": "^0.3.4",
|
||||
"rubellum/slim-blade-view": "^0.1.1",
|
||||
"tuupola/cors-middleware": "^0.7.0"
|
||||
"tuupola/cors-middleware": "^0.7.0",
|
||||
"eluceo/ical": "^0.15.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
53
composer.lock
generated
53
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "c1bc4c17739e9d0ee8b33628f6d4b9a4",
|
||||
"content-hash": "0b203f875499dfeaa61890cdec018a2d",
|
||||
"packages": [
|
||||
{
|
||||
"name": "container-interop/container-interop",
|
||||
@ -104,6 +104,57 @@
|
||||
],
|
||||
"time": "2018-01-09T20:05:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "eluceo/ical",
|
||||
"version": "0.15.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/markuspoerschke/iCal.git",
|
||||
"reference": "add0ca99aa1f77f134a2e8b071f2ebc22b115139"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/markuspoerschke/iCal/zipball/add0ca99aa1f77f134a2e8b071f2ebc22b115139",
|
||||
"reference": "add0ca99aa1f77f134a2e8b071f2ebc22b115139",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^6.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "Massive performance enhancement of line folding"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Eluceo\\iCal\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Markus Poerschke",
|
||||
"email": "markus@eluceo.de",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "The eluceo/iCal package offers a abstraction layer for creating iCalendars. You can easily create iCal files by using PHP object instead of typing your *.ics file by hand. The output will follow RFC 5545 as best as possible.",
|
||||
"homepage": "https://github.com/markuspoerschke/iCal",
|
||||
"keywords": [
|
||||
"calendar",
|
||||
"iCalendar",
|
||||
"ical",
|
||||
"ics",
|
||||
"php calendar"
|
||||
],
|
||||
"time": "2019-01-13T22:00:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "http-interop/http-factory",
|
||||
"version": "0.3.0",
|
||||
|
62
controllers/CalendarApiController.php
Normal file
62
controllers/CalendarApiController.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\CalendarService;
|
||||
use \Grocy\Services\ApiKeyService;
|
||||
|
||||
class CalendarApiController extends BaseApiController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->CalendarService = new CalendarService();
|
||||
$this->ApiKeyService = new ApiKeyService();
|
||||
}
|
||||
|
||||
protected $CalendarService;
|
||||
protected $ApiKeyService;
|
||||
|
||||
public function Ical(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
{
|
||||
$vCalendar = new \Eluceo\iCal\Component\Calendar('grocy');
|
||||
|
||||
$events = $this->CalendarService->GetEvents();
|
||||
foreach($events as $event)
|
||||
{
|
||||
$vEvent = new \Eluceo\iCal\Component\Event();
|
||||
$vEvent->setDtStart(new \DateTime($event['start']))
|
||||
->setDtEnd(new \DateTime($event['start']))
|
||||
->setSummary($event['title'])
|
||||
->setNoTime($event['date_format'] === 'date')
|
||||
->setUseUtc(false);
|
||||
|
||||
$vCalendar->addComponent($vEvent);
|
||||
}
|
||||
|
||||
$response->write($vCalendar->render());
|
||||
$response = $response->withHeader('Content-Type', 'text/calendar; charset=utf-8');
|
||||
return $response->withHeader('Content-Disposition', 'attachment; filename="grocy.ics"');
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->GenericErrorResponse($response, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function IcalSharingLink(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
{
|
||||
return $this->ApiResponse(array(
|
||||
'url' => $this->AppContainer->UrlManager->ConstructUrl('/api/calendar/ical?secret=' . $this->ApiKeyService->GetOrCreateApiKey(ApiKeyService::API_KEY_TYPE_SPECIAL_PURPOSE_CALENDAR_ICAL))
|
||||
));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->GenericErrorResponse($response, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -2,80 +2,22 @@
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use \Grocy\Services\StockService;
|
||||
use \Grocy\Services\TasksService;
|
||||
use \Grocy\Services\ChoresService;
|
||||
use \Grocy\Services\BatteriesService;
|
||||
use \Grocy\Services\CalendarService;
|
||||
|
||||
class CalendarController extends BaseController
|
||||
{
|
||||
public function __construct(\Slim\Container $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
$this->StockService = new StockService();
|
||||
$this->TasksService = new TasksService();
|
||||
$this->ChoresService = new ChoresService();
|
||||
$this->BatteriesService = new BatteriesService();
|
||||
$this->CalendarService = new CalendarService();
|
||||
}
|
||||
|
||||
protected $StockService;
|
||||
protected $TasksService;
|
||||
protected $ChoresService;
|
||||
protected $BatteriesService;
|
||||
protected $CalendarService;
|
||||
|
||||
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$products = $this->Database->products();
|
||||
$titlePrefix = $this->LocalizationService->Localize('Product expires') . ': ';
|
||||
$stockEvents = array();
|
||||
foreach($this->StockService->GetCurrentStock() as $currentStockEntry)
|
||||
{
|
||||
$stockEvents[] = array(
|
||||
'title' => $titlePrefix . FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name,
|
||||
'start' => $currentStockEntry->best_before_date
|
||||
);
|
||||
}
|
||||
|
||||
$titlePrefix = $this->LocalizationService->Localize('Task due') . ': ';
|
||||
$taskEvents = array();
|
||||
foreach($this->TasksService->GetCurrent() as $currentTaskEntry)
|
||||
{
|
||||
$taskEvents[] = array(
|
||||
'title' => $titlePrefix . $currentTaskEntry->name,
|
||||
'start' => $currentTaskEntry->due_date
|
||||
);
|
||||
}
|
||||
|
||||
$chores = $this->Database->chores();
|
||||
$titlePrefix = $this->LocalizationService->Localize('Chore due') . ': ';
|
||||
$choreEvents = array();
|
||||
foreach($this->ChoresService->GetCurrent() as $currentChoreEntry)
|
||||
{
|
||||
$choreEvents[] = array(
|
||||
'title' => $titlePrefix . FindObjectInArrayByPropertyValue($chores, 'id', $currentChoreEntry->chore_id)->name,
|
||||
'start' => $currentChoreEntry->next_estimated_execution_time
|
||||
);
|
||||
}
|
||||
|
||||
$batteries = $this->Database->batteries();
|
||||
$titlePrefix = $this->LocalizationService->Localize('Battery charge cycle due') . ': ';
|
||||
$batteryEvents = array();
|
||||
foreach($this->BatteriesService->GetCurrent() as $currentBatteryEntry)
|
||||
{
|
||||
$batteryEvents[] = array(
|
||||
'title' => $titlePrefix . FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->name,
|
||||
'start' => $currentBatteryEntry->next_estimated_charge_time
|
||||
);
|
||||
}
|
||||
|
||||
$fullcalendarEventSources = array();
|
||||
$fullcalendarEventSources[] = $stockEvents;
|
||||
$fullcalendarEventSources[] = $taskEvents;
|
||||
$fullcalendarEventSources[] = $choreEvents;
|
||||
$fullcalendarEventSources[] = $batteryEvents;
|
||||
|
||||
return $this->AppContainer->view->render($response, 'calendar', [
|
||||
'fullcalendarEventSources' => $fullcalendarEventSources
|
||||
'fullcalendarEventSources' => $this->CalendarService->GetEvents()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -1859,6 +1859,61 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/calendar/ical": {
|
||||
"get": {
|
||||
"summary": "Returns the calendar in iCal format",
|
||||
"tags": [
|
||||
"Calendar"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The iCal file contents",
|
||||
"content": {
|
||||
"text/calendar": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "The operation was not successful",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GenericErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/calendar/ical/sharing-link": {
|
||||
"get": {
|
||||
"summary": "Returns a (public) sharing link for the calendar in iCal format",
|
||||
"tags": [
|
||||
"Calendar"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The (public) sharing link for the calendar in iCal format",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
@ -333,5 +333,7 @@ return array(
|
||||
'Picture' => 'Picture',
|
||||
'Uncheck ingredients to not put them on the shopping list' => 'Uncheck ingredients to not put them on the shopping list',
|
||||
'This is for statistical purposes only' => 'This is for statistical purposes only',
|
||||
'You have to select a recipe' => 'You have to select a recipe'
|
||||
'You have to select a recipe' => 'You have to select a recipe',
|
||||
'Key type' => 'Key type',
|
||||
'Export as iCal' => 'Export as iCal'
|
||||
);
|
||||
|
@ -44,6 +44,18 @@ class ApiKeyAuthMiddleware extends BaseMiddleware
|
||||
$validApiKey = false;
|
||||
}
|
||||
|
||||
// Handling of special purpose API keys
|
||||
if (!$validApiKey)
|
||||
{
|
||||
if ($routeName === 'calendar-ical')
|
||||
{
|
||||
if ($request->getQueryParam('secret') !== null && $apiKeyService->IsValidApiKey($request->getQueryParam('secret'), ApiKeyService::API_KEY_TYPE_SPECIAL_PURPOSE_CALENDAR_ICAL))
|
||||
{
|
||||
$validApiKey = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$validSession && !$validApiKey)
|
||||
{
|
||||
define('GROCY_AUTHENTICATED', false);
|
||||
|
2
migrations/0056.sql
Normal file
2
migrations/0056.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE api_keys
|
||||
ADD key_type TEXT NOT NULL DEFAULT 'default';
|
@ -9,3 +9,19 @@
|
||||
"eventLimit": true,
|
||||
"eventSources": fullcalendarEventSources
|
||||
});
|
||||
|
||||
$("#ical-button").on("click", function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
Grocy.Api.Get('calendar/ical/sharing-link',
|
||||
function(result)
|
||||
{
|
||||
location.href = result.url;
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -182,6 +182,13 @@ $app->group('/api', function()
|
||||
$this->get('/tasks', '\Grocy\Controllers\TasksApiController:Current');
|
||||
$this->post('/tasks/{taskId}/complete', '\Grocy\Controllers\TasksApiController:MarkTaskAsCompleted');
|
||||
}
|
||||
|
||||
// Calendar
|
||||
if (GROCY_FEATURE_FLAG_CALENDAR)
|
||||
{
|
||||
$this->get('/calendar/ical', '\Grocy\Controllers\CalendarApiController:Ical')->setName('calendar-ical');
|
||||
$this->get('/calendar/ical/sharing-link', '\Grocy\Controllers\CalendarApiController:IcalSharingLink');
|
||||
}
|
||||
})->add(new ApiKeyAuthMiddleware($appContainer, $appContainer->LoginControllerInstance->GetSessionCookieName(), $appContainer->ApiKeyHeaderName))
|
||||
->add(JsonMiddleware::class)
|
||||
->add(new CorsMiddleware([
|
||||
|
@ -4,10 +4,13 @@ namespace Grocy\Services;
|
||||
|
||||
class ApiKeyService extends BaseService
|
||||
{
|
||||
const API_KEY_TYPE_DEFAULT = 'default';
|
||||
const API_KEY_TYPE_SPECIAL_PURPOSE_CALENDAR_ICAL = 'special-purpose-calendar-ical';
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function IsValidApiKey($apiKey)
|
||||
public function IsValidApiKey($apiKey, $keyType = self::API_KEY_TYPE_DEFAULT)
|
||||
{
|
||||
if ($apiKey === null || empty($apiKey))
|
||||
{
|
||||
@ -15,7 +18,7 @@ class ApiKeyService extends BaseService
|
||||
}
|
||||
else
|
||||
{
|
||||
$apiKeyRow = $this->Database->api_keys()->where('api_key = :1 AND expires > :2', $apiKey, date('Y-m-d H:i:s', time()))->fetch();
|
||||
$apiKeyRow = $this->Database->api_keys()->where('api_key = :1 AND expires > :2 AND key_type = :3', $apiKey, date('Y-m-d H:i:s', time()), $keyType)->fetch();
|
||||
if ($apiKeyRow !== null)
|
||||
{
|
||||
$apiKeyRow->update(array(
|
||||
@ -33,14 +36,15 @@ class ApiKeyService extends BaseService
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function CreateApiKey()
|
||||
public function CreateApiKey($keyType = self::API_KEY_TYPE_DEFAULT)
|
||||
{
|
||||
$newApiKey = $this->GenerateApiKey();
|
||||
|
||||
$apiKeyRow = $this->Database->api_keys()->createRow(array(
|
||||
'api_key' => $newApiKey,
|
||||
'user_id' => GROCY_USER_ID,
|
||||
'expires' => '2999-12-31 23:59:59' // Default is that API keys expire never
|
||||
'expires' => '2999-12-31 23:59:59', // Default is that API keys expire never
|
||||
'key_type' => $keyType
|
||||
));
|
||||
$apiKeyRow->save();
|
||||
|
||||
@ -68,6 +72,28 @@ class ApiKeyService extends BaseService
|
||||
return null;
|
||||
}
|
||||
|
||||
// Returns any valid key for $keyType,
|
||||
// not allowed for key type "default"
|
||||
public function GetOrCreateApiKey($keyType)
|
||||
{
|
||||
if ($keyType === self::API_KEY_TYPE_DEFAULT)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
$apiKeyRow = $this->Database->api_keys()->where('key_type = :1 AND expires > :2', $keyType, date('Y-m-d H:i:s', time()))->fetch();
|
||||
if ($apiKeyRow !== null)
|
||||
{
|
||||
return $apiKeyRow->api_key;
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->CreateApiKey($keyType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function GenerateApiKey()
|
||||
{
|
||||
return RandomString(50);
|
||||
|
80
services/CalendarService.php
Normal file
80
services/CalendarService.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Services;
|
||||
|
||||
use \Grocy\Services\StockService;
|
||||
use \Grocy\Services\TasksService;
|
||||
use \Grocy\Services\ChoresService;
|
||||
use \Grocy\Services\BatteriesService;
|
||||
|
||||
class CalendarService extends BaseService
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->StockService = new StockService();
|
||||
$this->TasksService = new TasksService();
|
||||
$this->ChoresService = new ChoresService();
|
||||
$this->BatteriesService = new BatteriesService();
|
||||
}
|
||||
|
||||
protected $StockService;
|
||||
protected $TasksService;
|
||||
protected $ChoresService;
|
||||
protected $BatteriesService;
|
||||
|
||||
public function GetEvents()
|
||||
{
|
||||
$products = $this->Database->products();
|
||||
$titlePrefix = $this->LocalizationService->Localize('Product expires') . ': ';
|
||||
$stockEvents = array();
|
||||
foreach($this->StockService->GetCurrentStock() as $currentStockEntry)
|
||||
{
|
||||
if ($currentStockEntry->amount > 0)
|
||||
{
|
||||
$stockEvents[] = array(
|
||||
'title' => $titlePrefix . FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name,
|
||||
'start' => $currentStockEntry->best_before_date,
|
||||
'date_format' => 'date'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$titlePrefix = $this->LocalizationService->Localize('Task due') . ': ';
|
||||
$taskEvents = array();
|
||||
foreach($this->TasksService->GetCurrent() as $currentTaskEntry)
|
||||
{
|
||||
$taskEvents[] = array(
|
||||
'title' => $titlePrefix . $currentTaskEntry->name,
|
||||
'start' => $currentTaskEntry->due_date,
|
||||
'date_format' => 'date'
|
||||
);
|
||||
}
|
||||
|
||||
$chores = $this->Database->chores();
|
||||
$titlePrefix = $this->LocalizationService->Localize('Chore due') . ': ';
|
||||
$choreEvents = array();
|
||||
foreach($this->ChoresService->GetCurrent() as $currentChoreEntry)
|
||||
{
|
||||
$choreEvents[] = array(
|
||||
'title' => $titlePrefix . FindObjectInArrayByPropertyValue($chores, 'id', $currentChoreEntry->chore_id)->name,
|
||||
'start' => $currentChoreEntry->next_estimated_execution_time,
|
||||
'date_format' => 'datetime'
|
||||
);
|
||||
}
|
||||
|
||||
$batteries = $this->Database->batteries();
|
||||
$titlePrefix = $this->LocalizationService->Localize('Battery charge cycle due') . ': ';
|
||||
$batteryEvents = array();
|
||||
foreach($this->BatteriesService->GetCurrent() as $currentBatteryEntry)
|
||||
{
|
||||
$batteryEvents[] = array(
|
||||
'title' => $titlePrefix . FindObjectInArrayByPropertyValue($batteries, 'id', $currentBatteryEntry->battery_id)->name,
|
||||
'start' => $currentBatteryEntry->next_estimated_charge_time,
|
||||
'date_format' => 'datetime'
|
||||
);
|
||||
}
|
||||
|
||||
return array_merge($stockEvents, $taskEvents, $choreEvents, $batteryEvents);
|
||||
}
|
||||
}
|
@ -16,12 +16,17 @@
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>@yield('title')</h1>
|
||||
<h1>
|
||||
@yield('title')
|
||||
<a id="ical-button" class="btn btn-outline-dark" href="#">
|
||||
<i class="fas fa-calendar-plus"></i> {{ $L('Export as iCal') }}
|
||||
</a>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var fullcalendarEventSources = {!! json_encode($fullcalendarEventSources) !!}
|
||||
var fullcalendarEventSources = {!! json_encode(array($fullcalendarEventSources)) !!}
|
||||
</script>
|
||||
|
||||
<div class="row">
|
||||
|
@ -38,6 +38,7 @@
|
||||
<th>{{ $L('Expires') }}</th>
|
||||
<th>{{ $L('Last used') }}</th>
|
||||
<th>{{ $L('Created') }}</th>
|
||||
<th>{{ $L('Key type') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="d-none">
|
||||
@ -66,6 +67,9 @@
|
||||
{{ $apiKey->row_created_timestamp }}
|
||||
<time class="timeago timeago-contextual" datetime="{{ $apiKey->row_created_timestamp }}"></time>
|
||||
</td>
|
||||
<td>
|
||||
{{ $apiKey->key_type }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
|
Loading…
x
Reference in New Issue
Block a user