Compare commits

...

10 Commits

Author SHA1 Message Date
Bernd Bestel
6f47a5415c Added a rudimentary habit analysis possibility 2018-07-25 20:01:58 +02:00
Bernd Bestel
42c1709633 Optimize and refactor latest changes 2018-07-25 19:28:15 +02:00
Bernd Bestel
4685ff4145 Add an update script for Linux 2018-07-25 08:22:27 +02:00
Bernd Bestel
249b01d7a8 Added possibility to track who did a habit (this implements and closes #21) 2018-07-24 20:45:14 +02:00
Bernd Bestel
bcbdf58376 Prefix all global vars 2018-07-24 19:41:35 +02:00
Bernd Bestel
7f8540ff4e Replace the single user (defined in /data/config.php) with a multi user management thing 2018-07-24 19:31:43 +02:00
Marius Boro
b52ab91606 Update no.php (#23)
* Update no.php

Better translation and minor typos

* Update no.php
2018-07-23 21:23:55 +02:00
Bernd Bestel
7246ac55b6 Hide scrollbar in sidebar for now (workaround for #15, found no better solution, this closes #15)
This also references BlackrockDigital/startbootstrap-sb-admin#73
2018-07-23 21:21:27 +02:00
Bernd Bestel
848931da21 Mention grocy-desktop in README 2018-07-22 13:59:53 +02:00
Bernd Bestel
bf4092e746 Changed latest release download link 2018-07-22 13:26:46 +02:00
60 changed files with 1292 additions and 283 deletions

View File

@@ -8,16 +8,24 @@ Public demo of the latest version → [https://demo.grocy.info](https://demo.
A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete houshold management"-thing. ERP your fridge!
## How to install
Just unpack the [latest release](https://github.com/berrnd/grocy/releases/latest) on your PHP (SQLite extension required, currently only tested with PHP 7.2) enabled webserver (webservers root should point to the `/public` directory), copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go.
> **NEW**
>
> There is now grocy-desktop if you want to run grocy without a webserver just like a normal (windows) desktop application.
>
> See https://github.com/berrnd/grocy-desktop or directly download the [latest release](https://releases.grocy.info/latest-desktop) - the installation is nothing more than just clicking 2 times "next"...
Default login is user `admin` with password `admin` - see the `data/config.php` file. Alternatively clone this repository and install Composer and Yarn dependencies manually.
Just unpack the [latest release](https://releases.grocy.info/latest) on your PHP (SQLite extension required, currently only tested with PHP 7.2) enabled webserver (webservers root should point to the `/public` directory), copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go. Default login is user `admin` with password `admin`, please change the password immediately (see user menu).
Alternatively clone this repository and install Composer and Yarn dependencies manually.
If you use nginx as your webserver, please include `try_files $uri /index.php;` in your location block.
If, however, your webserver does not support URL rewriting, set `DISABLE_URL_REWRITING` in `data/config.php` (`Setting('DISABLE_URL_REWRITING', true);`).
## How to update
Just overwrite everything with the latest release while keeping the `/data` directory, check `config-dist.php` for new configuration options and add them to your `data/config.php` (the default from values `config-dist.php` will be used for not in `data/config.php` defined settings).
Just overwrite everything with the latest release while keeping the `/data` directory, check `config-dist.php` for new configuration options and add them to your `data/config.php` (the default from values `config-dist.php` will be used for not in `data/config.php` defined settings). Just to be sure, please empty `/data/viewcache`.
If you run grocy on Linux, there is also `/update.sh` (beside the basic tools `unzip` is needed) which does exact this and additionally creates a backup (`.tgz` archive) of the current installation in `/data/backups` (backups older than 60 days will be deleted during the update).
## Localization
grocy is fully localizable - the default language is English (integrated into code), a German localization is always maintained by me. There is one file per language in the `localization` directory, if you want to create a translation, it's best to copy `localization/de.php` to a new one (e. g. `localization/it.php`) and translating all strings there. (Language can be changed in `data/config.php`, e. g. `Setting('CULTURE', 'it');`)

38
app.php
View File

@@ -6,17 +6,39 @@ use \Psr\Http\Message\ResponseInterface as Response;
use \Grocy\Helpers\UrlManager;
use \Grocy\Controllers\LoginController;
// Definitions for embedded mode
if (file_exists(__DIR__ . '/embedded.txt'))
{
define('DATAPATH', file_get_contents(__DIR__ . '/embedded.txt'));
define('GROCY_IS_EMBEDDED_INSTALL', true);
define('GROCY_DATAPATH', file_get_contents(__DIR__ . '/embedded.txt'));
define('GROCY_USER_ID', 1);
}
else
{
define('DATAPATH', __DIR__ . '/data');
define('GROCY_IS_EMBEDDED_INSTALL', false);
define('GROCY_DATAPATH', __DIR__ . '/data');
}
// Definitions for demo mode
if (file_exists(GROCY_DATAPATH . '/demo.txt'))
{
define('GROCY_IS_DEMO_INSTALL', true);
if (!defined('GROCY_USER_ID'))
{
define('GROCY_USER_ID', 1);
}
}
else
{
define('GROCY_IS_DEMO_INSTALL', false);
define('GROCY_DATAPATH', __DIR__ . '/data');
}
// Load composer dependencies
require_once __DIR__ . '/vendor/autoload.php';
require_once DATAPATH . '/config.php';
// Load config fils
require_once GROCY_DATAPATH . '/config.php';
require_once __DIR__ . '/config-dist.php'; //For not in own config defined values we use the default ones
// Setup base application
@@ -27,7 +49,7 @@ $appContainer = new \Slim\Container([
],
'view' => function($container)
{
return new \Slim\Views\Blade(__DIR__ . '/views', DATAPATH . '/viewcache');
return new \Slim\Views\Blade(__DIR__ . '/views', GROCY_DATAPATH . '/viewcache');
},
'LoginControllerInstance' => function($container)
{
@@ -35,7 +57,7 @@ $appContainer = new \Slim\Container([
},
'UrlManager' => function($container)
{
return new UrlManager(BASE_URL);
return new UrlManager(GROCY_BASE_URL);
},
'ApiKeyHeaderName' => function($container)
{
@@ -44,11 +66,7 @@ $appContainer = new \Slim\Container([
]);
$app = new \Slim\App($appContainer);
if (PHP_SAPI === 'cli')
{
$app->add(\pavlakis\cli\CliRequest::class);
}
// Load routes from separate file
require_once __DIR__ . '/routes.php';
$app->run();

View File

@@ -3,7 +3,6 @@
"php": ">=7.2",
"slim/slim": "^3.8",
"morris/lessql": "^0.3.4",
"pavlakis/slim-cli": "^1.0",
"rubellum/slim-blade-view": "^0.1.1",
"tuupola/cors-middleware": "^0.7.0"
},

85
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "131ab83ecb1ea3d1a431cc70b5092448",
"content-hash": "c1bc4c17739e9d0ee8b33628f6d4b9a4",
"packages": [
{
"name": "container-interop/container-interop",
@@ -158,7 +158,7 @@
},
{
"name": "illuminate/container",
"version": "v5.6.27",
"version": "v5.6.28",
"source": {
"type": "git",
"url": "https://github.com/illuminate/container.git",
@@ -202,7 +202,7 @@
},
{
"name": "illuminate/contracts",
"version": "v5.6.27",
"version": "v5.6.28",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
@@ -246,7 +246,7 @@
},
{
"name": "illuminate/events",
"version": "v5.6.27",
"version": "v5.6.28",
"source": {
"type": "git",
"url": "https://github.com/illuminate/events.git",
@@ -291,7 +291,7 @@
},
{
"name": "illuminate/filesystem",
"version": "v5.6.27",
"version": "v5.6.28",
"source": {
"type": "git",
"url": "https://github.com/illuminate/filesystem.git",
@@ -343,7 +343,7 @@
},
{
"name": "illuminate/support",
"version": "v5.6.27",
"version": "v5.6.28",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
@@ -401,7 +401,7 @@
},
{
"name": "illuminate/view",
"version": "v5.6.27",
"version": "v5.6.28",
"source": {
"type": "git",
"url": "https://github.com/illuminate/view.git",
@@ -651,55 +651,6 @@
],
"time": "2018-02-13T20:26:39+00:00"
},
{
"name": "pavlakis/slim-cli",
"version": "1.0.4",
"source": {
"type": "git",
"url": "https://github.com/pavlakis/slim-cli.git",
"reference": "603933a54e391b3c70c573206cce543b75d8b1db"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pavlakis/slim-cli/zipball/603933a54e391b3c70c573206cce543b75d8b1db",
"reference": "603933a54e391b3c70c573206cce543b75d8b1db",
"shasum": ""
},
"require": {
"php": "^5.5|^5.6|^7.0|^7.1"
},
"require-dev": {
"phpunit/phpunit": "^4.0",
"slim/slim": "^3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"pavlakis\\cli\\tests\\": "tests/phpunit",
"pavlakis\\cli\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Antonis Pavlakis",
"email": "adoni@pavlakis.info",
"homepage": "http://pavlakis.info"
}
],
"description": "Making a mock GET request through the CLI and enabling the same application entry point on CLI scripts.",
"homepage": "http://github.com/pavlakis/slim-cli",
"keywords": [
"cli",
"framework",
"middleware",
"slim"
],
"time": "2017-01-30T22:50:06+00:00"
},
{
"name": "philo/laravel-blade",
"version": "v3.1",
@@ -1214,16 +1165,16 @@
},
{
"name": "symfony/debug",
"version": "v4.1.1",
"version": "v4.1.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
"reference": "dbe0fad88046a755dcf9379f2964c61a02f5ae3d"
"reference": "a1f2118cedb8731c45e945cdd2b808ca82abc4b5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/dbe0fad88046a755dcf9379f2964c61a02f5ae3d",
"reference": "dbe0fad88046a755dcf9379f2964c61a02f5ae3d",
"url": "https://api.github.com/repos/symfony/debug/zipball/a1f2118cedb8731c45e945cdd2b808ca82abc4b5",
"reference": "a1f2118cedb8731c45e945cdd2b808ca82abc4b5",
"shasum": ""
},
"require": {
@@ -1266,11 +1217,11 @@
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
"time": "2018-06-08T09:39:36+00:00"
"time": "2018-07-06T14:52:28+00:00"
},
{
"name": "symfony/finder",
"version": "v4.1.1",
"version": "v4.1.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
@@ -1378,16 +1329,16 @@
},
{
"name": "symfony/translation",
"version": "v4.1.1",
"version": "v4.1.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "b6d8164085ee0b6debcd1b7a131fd6f63bb04854"
"reference": "2dd74d6b2dcbd46a93971e6ce7d245cf3123e957"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/b6d8164085ee0b6debcd1b7a131fd6f63bb04854",
"reference": "b6d8164085ee0b6debcd1b7a131fd6f63bb04854",
"url": "https://api.github.com/repos/symfony/translation/zipball/2dd74d6b2dcbd46a93971e6ce7d245cf3123e957",
"reference": "2dd74d6b2dcbd46a93971e6ce7d245cf3123e957",
"shasum": ""
},
"require": {
@@ -1443,7 +1394,7 @@
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
"time": "2018-06-22T08:59:39+00:00"
"time": "2018-07-23T08:20:20+00:00"
},
{
"name": "tuupola/callable-handler",

View File

@@ -1,9 +1,5 @@
<?php
# Login credentials
Setting('HTTP_USER', 'admin');
Setting('HTTP_PASSWORD', 'admin');
# Either "production" or "dev"
Setting('MODE', 'production');

View File

@@ -12,14 +12,13 @@ class BaseController
$databaseService = new DatabaseService();
$this->Database = $databaseService->GetDbConnection();
$localizationService = new LocalizationService(CULTURE);
$localizationService = new LocalizationService(GROCY_CULTURE);
$this->LocalizationService = $localizationService;
$applicationService = new ApplicationService();
$versionInfo = $applicationService->GetInstalledVersion();
$container->view->set('version', $versionInfo->Version);
$container->view->set('releaseDate', $versionInfo->ReleaseDate);
$container->view->set('isEmbeddedInstallation', $applicationService->IsEmbeddedInstallation());
$container->view->set('localizationStrings', $localizationService->GetCurrentCultureLocalizations());
$container->view->set('L', function($text, ...$placeholderValues) use($localizationService)

View File

@@ -1,19 +0,0 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\ApplicationService;
use \Grocy\Services\DatabaseMigrationService;
class CliController extends BaseController
{
public function RecreateDemo(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$applicationService = new ApplicationService();
if ($applicationService->IsDemoInstallation())
{
$databaseMigrationService = new DatabaseMigrationService();
$databaseMigrationService->RecreateDemo();
}
}
}

View File

@@ -22,9 +22,15 @@ class HabitsApiController extends BaseApiController
$trackedTime = $request->getQueryParams()['tracked_time'];
}
$doneBy = GROCY_USER_ID;
if (isset($request->getQueryParams()['done_by']) && !empty($request->getQueryParams()['done_by']))
{
$doneBy = $request->getQueryParams()['done_by'];
}
try
{
$this->HabitsService->TrackHabit($args['habitId'], $trackedTime);
$this->HabitsService->TrackHabit($args['habitId'], $trackedTime, $doneBy);
return $this->VoidApiActionResponse($response);
}
catch (\Exception $ex)

View File

@@ -38,7 +38,8 @@ class HabitsController extends BaseController
public function TrackHabitExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'habittracking', [
'habits' => $this->Database->habits()->orderBy('name')
'habits' => $this->Database->habits()->orderBy('name'),
'users' => $this->Database->users()->orderBy('username')
]);
}
@@ -49,6 +50,15 @@ class HabitsController extends BaseController
]);
}
public function Analysis(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'habitsanalysis', [
'habitsLog' => $this->Database->habits_log()->orderBy('tracked_time', 'DESC'),
'habits' => $this->Database->habits()->orderBy('name'),
'users' => $this->Database->users()->orderBy('username')
]);
}
public function HabitEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['habitId'] == 'new')

View File

@@ -3,7 +3,6 @@
namespace Grocy\Controllers;
use \Grocy\Services\SessionService;
use \Grocy\Services\ApplicationService;
use \Grocy\Services\DatabaseMigrationService;
use \Grocy\Services\DemoDataGeneratorService;
@@ -24,11 +23,21 @@ class LoginController extends BaseController
$postParams = $request->getParsedBody();
if (isset($postParams['username']) && isset($postParams['password']))
{
if ($postParams['username'] === HTTP_USER && $postParams['password'] === HTTP_PASSWORD)
$user = $this->Database->users()->where('username', $postParams['username'])->fetch();
$inputPassword = $postParams['password'];
if ($user !== null && password_verify($inputPassword, $user->password))
{
$sessionKey = $this->SessionService->CreateSession();
$sessionKey = $this->SessionService->CreateSession($user->id);
setcookie($this->SessionCookieName, $sessionKey, time() + 31536000); // Cookie expires in 1 year, but session validity is up to SessionService
if (password_needs_rehash($user->password, PASSWORD_DEFAULT))
{
$user->update(array(
'password' => password_hash($inputPassword, PASSWORD_DEFAULT)
));
}
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/'));
}
else
@@ -59,8 +68,7 @@ class LoginController extends BaseController
$databaseMigrationService = new DatabaseMigrationService();
$databaseMigrationService->MigrateDatabase();
$applicationService = new ApplicationService();
if ($applicationService->IsDemoInstallation())
if (GROCY_IS_DEMO_INSTALL)
{
$demoDataGeneratorService = new DemoDataGeneratorService();
$demoDataGeneratorService->PopulateDemoData();

View File

@@ -35,7 +35,8 @@ class OpenApiController extends BaseApiController
public function ApiKeysList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'manageapikeys', [
'apiKeys' => $this->Database->api_keys()
'apiKeys' => $this->Database->api_keys(),
'users' => $this->Database->users()
]);
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\UsersService;
class UsersApiController extends BaseApiController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->UsersService = new UsersService();
}
protected $UsersService;
public function GetUsers(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
return $this->ApiResponse($this->UsersService->GetUsersAsDto());
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function CreateUser(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$requestBody = $request->getParsedBody();
try
{
$this->UsersService->CreateUser($requestBody['username'], $requestBody['first_name'], $requestBody['last_name'], $requestBody['password']);
return $this->ApiResponse(array('success' => true));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function DeleteUser(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
$this->UsersService->DeleteUser($args['userId']);
return $this->ApiResponse(array('success' => true));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function EditUser(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$requestBody = $request->getParsedBody();
try
{
$this->UsersService->EditUser($args['userId'], $requestBody['username'], $requestBody['first_name'], $requestBody['last_name'], $requestBody['password']);
return $this->ApiResponse(array('success' => true));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Grocy\Controllers;
class UsersController extends BaseController
{
public function UsersList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'users', [
'users' => $this->Database->users()->orderBy('username')
]);
}
public function UserEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['userId'] == 'new')
{
return $this->AppContainer->view->render($response, 'userform', [
'mode' => 'create'
]);
}
else
{
return $this->AppContainer->view->render($response, 'userform', [
'user' => $this->Database->users($args['userId']),
'mode' => 'edit'
]);
}
}
}

View File

@@ -370,6 +370,173 @@
}
}
},
"/users/get": {
"get": {
"description": "Returns all users",
"tags": [
"User management"
],
"responses": {
"200": {
"description": "A list of user objects",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/UserDto"
}
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
}
},
"/users/create": {
"post": {
"description": "Creates a new user",
"tags": [
"User management"
],
"requestBody": {
"description": "A valid user object",
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"responses": {
"200": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VoidApiActionResponse"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
}
},
"/users/edit/{userId}": {
"post": {
"description": "Edits the given user",
"tags": [
"User management"
],
"parameters": [
{
"in": "path",
"name": "userId",
"required": true,
"description": "A valid user id",
"schema": {
"type": "integer"
}
}
],
"requestBody": {
"description": "A valid user object",
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"responses": {
"200": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VoidApiActionResponse"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
}
},
"/users/delete/{userId}": {
"get": {
"description": "Deletes the given user",
"tags": [
"User management"
],
"parameters": [
{
"in": "path",
"name": "userId",
"required": true,
"description": "A valid user id",
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VoidApiActionResponse"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
}
},
"/stock/add-product/{productId}/{amount}": {
"get": {
"description": "Adds the the given amount of the given product to stock",
@@ -776,6 +943,15 @@
"schema": {
"type": "date-time"
}
},
{
"in": "query",
"name": "done_by",
"required": false,
"description": "A valid user id of who executed this habit, when omitted, the currently authenticated user will be used",
"schema": {
"type": "integer"
}
}
],
"responses": {
@@ -1139,6 +1315,9 @@
"track_count": {
"type": "integer",
"description": "How often this habit was tracked so far"
},
"last_done_by": {
"$ref": "#/components/schemas/UserDto"
}
}
},
@@ -1182,6 +1361,55 @@
}
}
},
"User": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"username": {
"type": "string"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"password": {
"type": "string"
},
"row_created_timestamp": {
"type": "string",
"format": "date-time"
}
}
},
"UserDto": {
"type": "object",
"description": "A user object without the *password* and with an additional *display_name* property",
"properties": {
"id": {
"type": "integer"
},
"username": {
"type": "string"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"display_name": {
"type": "string"
},
"row_created_timestamp": {
"type": "string",
"format": "date-time"
}
}
},
"ApiKey": {
"type": "object",
"properties": {

View File

@@ -20,7 +20,7 @@ class UrlManager
public function ConstructUrl($relativePath, $isResource = false)
{
if (DISABLE_URL_REWRITING === false || $isResource === true)
if (GROCY_DISABLE_URL_REWRITING === false || $isResource === true)
{
return rtrim($this->BasePath, '/') . $relativePath;
}

View File

@@ -130,17 +130,41 @@ function BoolToString(bool $bool)
function Setting(string $name, $value)
{
if (!defined($name))
if (!defined('GROCY_' . $name))
{
// The content of a $name.txt file in /data/settingoverrides can overwrite the given setting (for embedded mode)
$settingOverrideFile = DATAPATH . '/settingoverrides/' . $name . '.txt';
$settingOverrideFile = GROCY_DATAPATH . '/settingoverrides/' . $name . '.txt';
if (file_exists($settingOverrideFile))
{
define($name, file_get_contents($settingOverrideFile));
define('GROCY_' . $name, file_get_contents($settingOverrideFile));
}
else
{
define($name, $value);
define('GROCY_' . $name, $value);
}
}
}
function GetUserDisplayName($user)
{
$displayName = '';
if (empty($user->first_name) && !empty($user->last_name))
{
$displayName = $user->last_name;
}
elseif (empty($user->last_name) && !empty($user->first_name))
{
$displayName = $user->first_name;
}
elseif (!empty($user->last_name) && !empty($user->first_name))
{
$displayName = $user->first_name . ' ' . $user->last_name;
}
else
{
$displayName = $user->username;
}
return $displayName;
}

View File

@@ -169,6 +169,23 @@ return array(
'Put missing amount on shopping list' => 'Fehlende Menge auf den Einkaufszettel setzen',
'Are you sure to put all missing ingredients for recipe "#1" on the shopping list?' => 'Sicher alle fehlenden Zutaten für Rezept "#1" auf die Einkaufsliste zu setzen?',
'Added for recipe #1' => 'Hinzugefügt für Rezept #1',
'Manage users' => 'Benutzer verwalten',
'User' => 'Benutzer',
'Users' => 'Benutzer',
'Are you sure to delete user "#1"?' => 'Benutzer "#1" wirklich löschen?',
'Create user' => 'Benutzer erstellen',
'Edit user' => 'Benutzer bearbeiten',
'First name' => 'Vorname',
'Last name' => 'Nachname',
'A username is required' => 'Ein Benutzername ist erforderlich',
'Confirm password' => 'Passwort bestätigen',
'Passwords do not match' => 'Passwörter stimmen nicht überein',
'Change password' => 'Passwort ändern',
'Done by' => 'Ausgeführt von',
'Last done by' => 'Zuletzt ausgeführt von',
'Unknown' => 'Unbekannt',
'Filter by habit' => 'Nach Gewohnheit filtern',
'Habits analysis' => 'Gewohnheiten Analyse',
//Constants
'manually' => 'Manuell',
@@ -226,5 +243,6 @@ return array(
'German' => 'Deutsch',
'Italian' => 'Italienisch',
'Demo in different language' => 'Demo in anderer Sprache',
'This is the note content of the recipe ingredient' => 'Dies ist der Inhalt der Notiz der Zutat'
'This is the note content of the recipe ingredient' => 'Dies ist der Inhalt der Notiz der Zutat',
'Demo User' => 'Demo Benutzer'
);

View File

@@ -9,7 +9,7 @@ return array(
'Product' => 'Produkt',
'Amount' => 'Antall',
'Next best before date' => 'Kommende best før dato',
'Logout' => 'Logg Av',
'Logout' => 'Logg ut',
'Habits overview' => 'Oversikt Husoppgaver',
'Batteries overview' => 'Oversikt Batteri',
'Purchase' => 'Innkjøp',
@@ -17,7 +17,7 @@ return array(
'Inventory' => 'Endre Husholdning',
'Shopping list' => 'Handleliste',
'Habit tracking' => 'Logge Husoppgaver',
'Battery tracking' => 'Ladesyklus Batteri',
'Battery tracking' => 'Batteri Ladesyklus',
'Products' => 'Produkter',
'Locations' => 'Lokasjoner',
'Quantity units' => 'Forpakning',
@@ -32,7 +32,7 @@ return array(
'Best before' => 'Best før',
'OK' => 'OK',
'Product overview' => 'Oversikt Produkt',
'Stock quantity unit' => 'Forpakning i husholdningen',
'Stock quantity unit' => 'Forpakningstype i husholdningen',
'Stock amount' => 'Husholdning',
'Last purchased' => 'Sist kjøpt',
'Last used' => 'Sist brukt',
@@ -67,8 +67,8 @@ return array(
'Factor purchase to stock quantity unit' => 'Innkjøpsfaktor for forpakning',
'Create location' => 'Opprett lokasjon',
'Create quantity unit' => 'Opprett forpakning',
'Period type' => 'Periodetype',
'Period days' => 'Dager/ Periode',
'Period type' => 'Gjentakelse',
'Period days' => 'Dager for gjentakelse',
'Create habit' => 'Opprett husoppgave',
'Used in' => 'Brukt',
'Create battery' => 'Opprett batteri',
@@ -133,7 +133,7 @@ return array(
'Search' => 'Søk',
'Not logged in' => 'Ikke logget inn',
'You have to select a product' => 'Du må velge et produkt',
'You have to select a habit' => 'Du mpå velge en husoppgaven',
'You have to select a habit' => 'Du må velge en husoppgaven',
'You have to select a battery' => 'Du må velge et batteri',
'A name is required' => 'Et navn kreves',
'A location is required' => 'En lokasjon kreves',
@@ -142,7 +142,7 @@ return array(
'A quantity unit is required' => 'Forpakning antall/størrelse kreves',
'A period type is required' => 'En periodetype kreves',
'A best before date is required and must be later than today' => 'En best før dato kreves, denne må være senere enn i dag',
'Settings' => 'Instillinger',
'Settings' => 'Innstillinger',
'This can only be before now' => 'Dette kan kun være før nå',
'Calendar' => 'Kalender',
'Recipes' => 'Oppskrifter',
@@ -157,12 +157,12 @@ return array(
'Clear list' => 'Tøm liste',
'Requirements fulfilled' => 'Krav oppfylt',
'Put missing products on shopping list' => 'Legg manglende produkter til handlelisten',
'Not enough in stock, #1 ingredients missing' => 'Ikke nok i husholdningen, #1 ingrediens mangler',
'Not enough in stock, #1 ingredients missing' => 'Ikke nok i husholdningen, #1 ingredienser mangler',
'Enough in stock' => 'Nok i husholdningen',
'Not enough in stock, #1 ingredients missing but already on the shopping list' => 'Ikke nok i husholdningen, #1 ingrediens mangler, men lagt til i handelisten',
'Not enough in stock, #1 ingredients missing but already on the shopping list' => 'Ikke nok i husholdningen, #1 ingrediens mangler, men står allerede på handelisten',
'Expand to fullscreen' => 'Full skjerm',
'Ingredients' => 'Ingredienser',
'Preparation' => 'Forberedelse',
'Preparation' => 'Forberedelse / Slik gjør du',
'Recipe' => 'Oppskrift',
'Not enough in stock, #1 missing, #2 already on shopping list' => 'Ikke nok i husholdningen, #1 mangler, #2 allerede i handlisten',
'Show notes' => 'Vis notater',
@@ -172,7 +172,7 @@ return array(
//Constants
'manually' => 'Manuel',
'dynamic-regular' => 'Dynamisk Regulering',
'dynamic-regular' => 'Automatisk (rullering settes under)',
//Technical component translations
'timeago_locale' => 'no',

View File

@@ -22,8 +22,9 @@ class ApiKeyAuthMiddleware extends BaseMiddleware
$route = $request->getAttribute('route');
$routeName = $route->getName();
if ($this->ApplicationService->IsDemoInstallation() || $this->ApplicationService->IsEmbeddedInstallation())
if (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL)
{
define('GROCY_AUTHENTICATED', true);
$response = $next($request, $response);
}
else
@@ -45,10 +46,15 @@ class ApiKeyAuthMiddleware extends BaseMiddleware
if (!$validSession && !$validApiKey)
{
define('GROCY_AUTHENTICATED', false);
$response = $response->withStatus(401);
}
else
{
$user = $apiKeyService->GetUserByApiKey($request->getHeaderLine($this->ApiKeyHeaderName));
define('GROCY_AUTHENTICATED', true);
define('GROCY_USER_ID', $user->id);
$response = $next($request, $response);
}
}

View File

@@ -1,20 +0,0 @@
<?php
namespace Grocy\Middleware;
class CliMiddleware extends BaseMiddleware
{
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next)
{
if (PHP_SAPI !== 'cli')
{
$response->write('Please call this only from CLI');
return $response->withHeader('Content-Type', 'text/plain')->withStatus(400);
}
else
{
$response = $next($request, $response);
return $response->withHeader('Content-Type', 'text/plain');
}
}
}

View File

@@ -3,6 +3,7 @@
namespace Grocy\Middleware;
use \Grocy\Services\SessionService;
use \Grocy\Services\LocalizationService;
class SessionAuthMiddleware extends BaseMiddleware
{
@@ -18,23 +19,42 @@ class SessionAuthMiddleware extends BaseMiddleware
{
$route = $request->getAttribute('route');
$routeName = $route->getName();
$sessionService = new SessionService();
if ($routeName === 'root' || $this->ApplicationService->IsDemoInstallation() || $this->ApplicationService->IsEmbeddedInstallation())
if ($routeName === 'root')
{
define('AUTHENTICATED', $this->ApplicationService->IsDemoInstallation() || $this->ApplicationService->IsEmbeddedInstallation());
$response = $next($request, $response);
}
elseif (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL)
{
$user = $sessionService->GetDefaultUser();
define('GROCY_AUTHENTICATED', true);
define('GROCY_USER_USERNAME', $user->username);
define('GROCY_USER_ID', $user->id);
$response = $next($request, $response);
}
else
{
$sessionService = new SessionService();
if ((!isset($_COOKIE[$this->SessionCookieName]) || !$sessionService->IsValidSession($_COOKIE[$this->SessionCookieName])) && $routeName !== 'login')
{
define('AUTHENTICATED', false);
define('GROCY_AUTHENTICATED', false);
$response = $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login'));
}
else
{
define('AUTHENTICATED', $routeName !== 'login');
if ($routeName !== 'login')
{
$user = $sessionService->GetUserBySessionKey($_COOKIE[$this->SessionCookieName]);
define('GROCY_AUTHENTICATED', true);
define('GROCY_USER_USERNAME', $user->username);
define('GROCY_USER_ID', $user->id);
}
else
{
define('GROCY_AUTHENTICATED', false);
}
$response = $next($request, $response);
}
}

20
migrations/0026.sql Normal file
View File

@@ -0,0 +1,20 @@
CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
username TEXT NOT NULL UNIQUE,
first_name TEXT,
last_name TEXT,
password TEXT NOT NULL,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
);
DROP TABLE sessions;
CREATE TABLE sessions (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
session_key TEXT NOT NULL UNIQUE,
user_id INTEGER NOT NULL,
expires DATETIME,
last_used DATETIME,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

24
migrations/0027.php Normal file
View File

@@ -0,0 +1,24 @@
<?php
// This is executed inside DatabaseMigrationService class/context
$db = $this->DatabaseService->GetDbConnection();
if (defined('HTTP_USER'))
{
// Migrate old user defined in config file to database
$newUserRow = $db->users()->createRow(array(
'username' => HTTP_USER,
'password' => password_hash(HTTP_PASSWORD, PASSWORD_DEFAULT)
));
$newUserRow->save();
}
else
{
// Create default user "admin" with password "admin"
$newUserRow = $db->users()->createRow(array(
'username' => 'admin',
'password' => password_hash('admin', PASSWORD_DEFAULT)
));
$newUserRow->save();
}

13
migrations/0028.sql Normal file
View File

@@ -0,0 +1,13 @@
ALTER TABLE habits_log
ADD done_by_user_id;
DROP TABLE api_keys;
CREATE TABLE api_keys (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
api_key TEXT NOT NULL UNIQUE,
user_id INTEGER NOT NULL,
expires DATETIME,
last_used DATETIME,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
);

View File

@@ -70,8 +70,7 @@ a.discrete-link:focus {
}
.navbar-sidenav {
overflow-y: auto;
overflow-x: hidden;
overflow: hidden;
border-top: 2px solid !important;
}
@@ -175,11 +174,6 @@ td {
width: auto !important;
}
/* Third party component customizations - Popper.js */
.tooltip .arrow {
display: none;
}
/* Third party component customizations - Bootstrap Combobox */
.typeahead .active {
background-color: #e5e5e5;

View File

@@ -7,7 +7,7 @@ Grocy.Components.BatteryCard.Refresh = function(batteryId)
{
$('#batterycard-battery-name').text(batteryDetails.battery.name);
$('#batterycard-battery-used_in').text(batteryDetails.battery.used_in);
$('#batterycard-battery-last-charged').text((batteryDetails.last_charged || 'never'));
$('#batterycard-battery-last-charged').text((batteryDetails.last_charged || L('never')));
$('#batterycard-battery-last-charged-timeago').text($.timeago(batteryDetails.last_charged || ''));
$('#batterycard-battery-charge-cycles-count').text((batteryDetails.charge_cycles_count || '0'));

View File

@@ -6,9 +6,10 @@ Grocy.Components.HabitCard.Refresh = function(habitId)
function(habitDetails)
{
$('#habitcard-habit-name').text(habitDetails.habit.name);
$('#habitcard-habit-last-tracked').text((habitDetails.last_tracked || 'never'));
$('#habitcard-habit-last-tracked').text((habitDetails.last_tracked || L('never')));
$('#habitcard-habit-last-tracked-timeago').text($.timeago(habitDetails.last_tracked || ''));
$('#habitcard-habit-tracked-count').text((habitDetails.tracked_count || '0'));
$('#habitcard-habit-last-done-by').text((habitDetails.last_done_by.display_name || L('Unknown')));
EmptyElementWhenMatches('#habitcard-habit-last-tracked-timeago', L('timeago_nan'));
},

View File

@@ -1,6 +1,6 @@
Grocy.Components.ProductPicker = { };
Grocy.Components.ProductPicker.GetPicker = function ()
Grocy.Components.ProductPicker.GetPicker = function()
{
return $('#product_id');
}
@@ -43,7 +43,7 @@ Grocy.Components.ProductPicker.HideCustomError = function()
$("#custom-productpicker-error").addClass("d-none");
}
$('.combobox').combobox({
$('.product-combobox').combobox({
appendId: '_text_input',
bsVersion: '4'
});

View File

@@ -0,0 +1,62 @@
Grocy.Components.UserPicker = { };
Grocy.Components.UserPicker.GetPicker = function()
{
return $('#user_id');
}
Grocy.Components.UserPicker.GetInputElement = function()
{
return $('#user_id_text_input');
}
Grocy.Components.UserPicker.GetValue = function()
{
return $('#user_id').val();
}
Grocy.Components.UserPicker.SetValue = function(value)
{
Grocy.Components.UserPicker.GetInputElement().val(value);
Grocy.Components.UserPicker.GetInputElement().trigger('change');
}
$('.user-combobox').combobox({
appendId: '_text_input',
bsVersion: '4'
});
var prefillUser = Grocy.Components.UserPicker.GetPicker().parent().data('prefill-by-username').toString();
if (typeof prefillUser !== "undefined")
{
var possibleOptionElement = $("#user_id option[data-additional-searchdata*='" + prefillUser + "']").first();
if (possibleOptionElement.length === 0)
{
possibleOptionElement = $("#user_id option:contains('" + prefillUser + "')").first();
}
if (possibleOptionElement.length > 0)
{
$('#user_id').val(possibleOptionElement.val());
$('#user_id').data('combobox').refresh();
$('#user_id').trigger('change');
var nextInputElement = $(Grocy.Components.UserPicker.GetPicker().parent().data('next-input-selector').toString());
nextInputElement.focus();
}
}
var prefillUserId = Grocy.Components.UserPicker.GetPicker().parent().data('prefill-by-user-id').toString();
if (typeof prefillUserId !== "undefined")
{
var possibleOptionElement = $("#user_id option[value='" + prefillUserId + "']").first();
if (possibleOptionElement.length > 0)
{
$('#user_id').val(possibleOptionElement.val());
$('#user_id').data('combobox').refresh();
$('#user_id').trigger('change');
var nextInputElement = $(Grocy.Components.UserPicker.GetPicker().parent().data('next-input-selector').toString());
nextInputElement.focus();
}
}

View File

@@ -0,0 +1,37 @@
var habitsAnalysisTable = $('#habits-analysis-table').DataTable({
'paginate': false,
'order': [[1, 'desc']],
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true
});
$("#habit-filter").on("change", function()
{
var value = $(this).val();
var text = $("#habit-filter option:selected").text();
if (value === "all")
{
text = "";
}
habitsAnalysisTable.column(0).search(text).draw();
});
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
habitsAnalysisTable.search(value).draw();
});
if (typeof GetUriParam("habit") !== "undefined")
{
$("#habit-filter").val(GetUriParam("habit"));
$("#habit-filter").trigger("change");
}

View File

@@ -7,7 +7,7 @@
Grocy.Api.Get('habits/get-habit-details/' + jsonForm.habit_id,
function (habitDetails)
{
Grocy.Api.Get('habits/track-habit-execution/' + jsonForm.habit_id + '?tracked_time=' + Grocy.Components.DateTimePicker.GetValue(),
Grocy.Api.Get('habits/track-habit-execution/' + jsonForm.habit_id + '?tracked_time=' + Grocy.Components.DateTimePicker.GetValue() + "&done_by=" + Grocy.Components.UserPicker.GetValue(),
function(result)
{
toastr.success(L('Tracked execution of habit #1 on #2', habitDetails.habit.name, Grocy.Components.DateTimePicker.GetValue()));

73
public/viewjs/userform.js Normal file
View File

@@ -0,0 +1,73 @@
$('#save-user-button').on('click', function(e)
{
e.preventDefault();
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('users/create', $('#user-form').serializeJSON(),
function(result)
{
window.location.href = U('/users');
},
function(xhr)
{
console.error(xhr);
}
);
}
else
{
Grocy.Api.Post('users/edit/' + Grocy.EditObjectId, $('#user-form').serializeJSON(),
function(result)
{
window.location.href = U('/users');
},
function(xhr)
{
console.error(xhr);
}
);
}
});
$('#user-form input').keyup(function (event)
{
var element = document.getElementById("password_confirm");
if ($("#password").val() !== $("#password_confirm").val())
{
element.setCustomValidity("error");
}
else
{
element.setCustomValidity("");
}
Grocy.FrontendHelpers.ValidateForm('user-form');
});
$('#user-form input').keydown(function (event)
{
if (event.keyCode === 13) //Enter
{
if (document.getElementById('user-form').checkValidity() === false) //There is at least one validation error
{
event.preventDefault();
return false;
}
else
{
$('#save-user-button').click();
}
}
});
if (GetUriParam("changepw") === "true")
{
$('#password').focus();
}
else
{
$('#username').focus();
}
Grocy.FrontendHelpers.ValidateForm('user-form');

58
public/viewjs/users.js Normal file
View File

@@ -0,0 +1,58 @@
var usersTable = $('#users-table').DataTable({
'paginate': false,
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
],
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true
});
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
usersTable.search(value).draw();
});
$(document).on('click', '.user-delete-button', function (e)
{
var objectName = $(e.currentTarget).attr('data-user-username');
var objectId = $(e.currentTarget).attr('data-user-id');
bootbox.confirm({
message: L('Are you sure to delete user "#1"?', objectName),
buttons: {
confirm: {
label: L('Yes'),
className: 'btn-success'
},
cancel: {
label: L('No'),
className: 'btn-danger'
}
},
callback: function(result)
{
if (result === true)
{
Grocy.Api.Get('users/delete/' + objectId,
function(result)
{
window.location.href = U('/users');
},
function(xhr)
{
console.error(xhr);
}
);
}
}
});
});

View File

@@ -1,7 +1,6 @@
<?php
use \Grocy\Middleware\JsonMiddleware;
use \Grocy\Middleware\CliMiddleware;
use \Grocy\Middleware\SessionAuthMiddleware;
use \Grocy\Middleware\ApiKeyAuthMiddleware;
use \Tuupola\Middleware\CorsMiddleware;
@@ -16,75 +15,88 @@ $app->group('', function()
$this->post('/login', 'LoginControllerInstance:ProcessLogin')->setName('login');
$this->get('/logout', 'LoginControllerInstance:Logout');
// User routes
$this->get('/users', '\Grocy\Controllers\UsersController:UsersList');
$this->get('/user/{userId}', '\Grocy\Controllers\UsersController:UserEditForm');
// Stock routes
$this->get('/stockoverview', 'Grocy\Controllers\StockController:Overview');
$this->get('/purchase', 'Grocy\Controllers\StockController:Purchase');
$this->get('/consume', 'Grocy\Controllers\StockController:Consume');
$this->get('/inventory', 'Grocy\Controllers\StockController:Inventory');
$this->get('/products', 'Grocy\Controllers\StockController:ProductsList');
$this->get('/product/{productId}', 'Grocy\Controllers\StockController:ProductEditForm');
$this->get('/locations', 'Grocy\Controllers\StockController:LocationsList');
$this->get('/location/{locationId}', 'Grocy\Controllers\StockController:LocationEditForm');
$this->get('/quantityunits', 'Grocy\Controllers\StockController:QuantityUnitsList');
$this->get('/quantityunit/{quantityunitId}', 'Grocy\Controllers\StockController:QuantityUnitEditForm');
$this->get('/shoppinglist', 'Grocy\Controllers\StockController:ShoppingList');
$this->get('/shoppinglistitem/{itemId}', 'Grocy\Controllers\StockController:ShoppingListItemEditForm');
$this->get('/stockoverview', '\Grocy\Controllers\StockController:Overview');
$this->get('/purchase', '\Grocy\Controllers\StockController:Purchase');
$this->get('/consume', '\Grocy\Controllers\StockController:Consume');
$this->get('/inventory', '\Grocy\Controllers\StockController:Inventory');
$this->get('/products', '\Grocy\Controllers\StockController:ProductsList');
$this->get('/product/{productId}', '\Grocy\Controllers\StockController:ProductEditForm');
$this->get('/locations', '\Grocy\Controllers\StockController:LocationsList');
$this->get('/location/{locationId}', '\Grocy\Controllers\StockController:LocationEditForm');
$this->get('/quantityunits', '\Grocy\Controllers\StockController:QuantityUnitsList');
$this->get('/quantityunit/{quantityunitId}', '\Grocy\Controllers\StockController:QuantityUnitEditForm');
$this->get('/shoppinglist', '\Grocy\Controllers\StockController:ShoppingList');
$this->get('/shoppinglistitem/{itemId}', '\Grocy\Controllers\StockController:ShoppingListItemEditForm');
// Recipe routes
$this->get('/recipes', 'Grocy\Controllers\RecipesController:Overview');
$this->get('/recipe/{recipeId}', 'Grocy\Controllers\RecipesController:RecipeEditForm');
$this->get('/recipe/{recipeId}/pos/{recipePosId}', 'Grocy\Controllers\RecipesController:RecipePosEditForm');
$this->get('/recipes', '\Grocy\Controllers\RecipesController:Overview');
$this->get('/recipe/{recipeId}', '\Grocy\Controllers\RecipesController:RecipeEditForm');
$this->get('/recipe/{recipeId}/pos/{recipePosId}', '\Grocy\Controllers\RecipesController:RecipePosEditForm');
// Habit routes
$this->get('/habitsoverview', 'Grocy\Controllers\HabitsController:Overview');
$this->get('/habittracking', 'Grocy\Controllers\HabitsController:TrackHabitExecution');
$this->get('/habitsoverview', '\Grocy\Controllers\HabitsController:Overview');
$this->get('/habittracking', '\Grocy\Controllers\HabitsController:TrackHabitExecution');
$this->get('/habitsanalysis', '\Grocy\Controllers\HabitsController:Analysis');
$this->get('/habits', 'Grocy\Controllers\HabitsController:HabitsList');
$this->get('/habit/{habitId}', 'Grocy\Controllers\HabitsController:HabitEditForm');
$this->get('/habits', '\Grocy\Controllers\HabitsController:HabitsList');
$this->get('/habit/{habitId}', '\Grocy\Controllers\HabitsController:HabitEditForm');
// Battery routes
$this->get('/batteriesoverview', 'Grocy\Controllers\BatteriesController:Overview');
$this->get('/batterytracking', 'Grocy\Controllers\BatteriesController:TrackChargeCycle');
$this->get('/batteriesoverview', '\Grocy\Controllers\BatteriesController:Overview');
$this->get('/batterytracking', '\Grocy\Controllers\BatteriesController:TrackChargeCycle');
$this->get('/batteries', 'Grocy\Controllers\BatteriesController:BatteriesList');
$this->get('/battery/{batteryId}', 'Grocy\Controllers\BatteriesController:BatteryEditForm');
$this->get('/batteries', '\Grocy\Controllers\BatteriesController:BatteriesList');
$this->get('/battery/{batteryId}', '\Grocy\Controllers\BatteriesController:BatteryEditForm');
// Other routes
$this->get('/api', 'Grocy\Controllers\OpenApiController:DocumentationUi');
$this->get('/manageapikeys', 'Grocy\Controllers\OpenApiController:ApiKeysList');
$this->get('/manageapikeys/new', 'Grocy\Controllers\OpenApiController:CreateNewApiKey');
// OpenAPI routes
$this->get('/api', '\Grocy\Controllers\OpenApiController:DocumentationUi');
$this->get('/manageapikeys', '\Grocy\Controllers\OpenApiController:ApiKeysList');
$this->get('/manageapikeys/new', '\Grocy\Controllers\OpenApiController:CreateNewApiKey');
})->add(new SessionAuthMiddleware($appContainer, $appContainer->LoginControllerInstance->GetSessionCookieName()));
$app->group('/api', function()
{
$this->get('/get-openapi-specification', 'Grocy\Controllers\OpenApiController:DocumentationSpec');
// OpenAPI
$this->get('/get-openapi-specification', '\Grocy\Controllers\OpenApiController:DocumentationSpec');
$this->get('/get-objects/{entity}', 'Grocy\Controllers\GenericEntityApiController:GetObjects');
$this->get('/get-object/{entity}/{objectId}', 'Grocy\Controllers\GenericEntityApiController:GetObject');
$this->post('/add-object/{entity}', 'Grocy\Controllers\GenericEntityApiController:AddObject');
$this->post('/edit-object/{entity}/{objectId}', 'Grocy\Controllers\GenericEntityApiController:EditObject');
$this->get('/delete-object/{entity}/{objectId}', 'Grocy\Controllers\GenericEntityApiController:DeleteObject');
// Generic entity interaction
$this->get('/get-objects/{entity}', '\Grocy\Controllers\GenericEntityApiController:GetObjects');
$this->get('/get-object/{entity}/{objectId}', '\Grocy\Controllers\GenericEntityApiController:GetObject');
$this->post('/add-object/{entity}', '\Grocy\Controllers\GenericEntityApiController:AddObject');
$this->post('/edit-object/{entity}/{objectId}', '\Grocy\Controllers\GenericEntityApiController:EditObject');
$this->get('/delete-object/{entity}/{objectId}', '\Grocy\Controllers\GenericEntityApiController:DeleteObject');
$this->get('/stock/add-product/{productId}/{amount}', 'Grocy\Controllers\StockApiController:AddProduct');
$this->get('/stock/consume-product/{productId}/{amount}', 'Grocy\Controllers\StockApiController:ConsumeProduct');
$this->get('/stock/inventory-product/{productId}/{newAmount}', 'Grocy\Controllers\StockApiController:InventoryProduct');
$this->get('/stock/get-product-details/{productId}', 'Grocy\Controllers\StockApiController:ProductDetails');
$this->get('/stock/get-current-stock', 'Grocy\Controllers\StockApiController:CurrentStock');
$this->get('/stock/add-missing-products-to-shoppinglist', 'Grocy\Controllers\StockApiController:AddMissingProductsToShoppingList');
$this->get('/stock/clear-shopping-list', 'Grocy\Controllers\StockApiController:ClearShoppingList');
$this->get('/stock/external-barcode-lookup/{barcode}', 'Grocy\Controllers\StockApiController:ExternalBarcodeLookup');
// Users
$this->get('/users/get', '\Grocy\Controllers\UsersApiController:GetUsers');
$this->post('/users/create', '\Grocy\Controllers\UsersApiController:CreateUser');
$this->post('/users/edit/{userId}', '\Grocy\Controllers\UsersApiController:EditUser');
$this->get('/users/delete/{userId}', '\Grocy\Controllers\UsersApiController:DeleteUser');
$this->get('/recipes/add-not-fulfilled-products-to-shopping-list/{recipeId}', 'Grocy\Controllers\RecipesApiController:AddNotFulfilledProductsToShoppingList');
// Stock
$this->get('/stock/add-product/{productId}/{amount}', '\Grocy\Controllers\StockApiController:AddProduct');
$this->get('/stock/consume-product/{productId}/{amount}', '\Grocy\Controllers\StockApiController:ConsumeProduct');
$this->get('/stock/inventory-product/{productId}/{newAmount}', '\Grocy\Controllers\StockApiController:InventoryProduct');
$this->get('/stock/get-product-details/{productId}', '\Grocy\Controllers\StockApiController:ProductDetails');
$this->get('/stock/get-current-stock', '\Grocy\Controllers\StockApiController:CurrentStock');
$this->get('/stock/add-missing-products-to-shoppinglist', '\Grocy\Controllers\StockApiController:AddMissingProductsToShoppingList');
$this->get('/stock/clear-shopping-list', '\Grocy\Controllers\StockApiController:ClearShoppingList');
$this->get('/stock/external-barcode-lookup/{barcode}', '\Grocy\Controllers\StockApiController:ExternalBarcodeLookup');
$this->get('/habits/track-habit-execution/{habitId}', 'Grocy\Controllers\HabitsApiController:TrackHabitExecution');
$this->get('/habits/get-habit-details/{habitId}', 'Grocy\Controllers\HabitsApiController:HabitDetails');
// Recipes
$this->get('/recipes/add-not-fulfilled-products-to-shopping-list/{recipeId}', '\Grocy\Controllers\RecipesApiController:AddNotFulfilledProductsToShoppingList');
// Habits
$this->get('/habits/track-habit-execution/{habitId}', '\Grocy\Controllers\HabitsApiController:TrackHabitExecution');
$this->get('/habits/get-habit-details/{habitId}', '\Grocy\Controllers\HabitsApiController:HabitDetails');
$this->get('/batteries/track-charge-cycle/{batteryId}', 'Grocy\Controllers\BatteriesApiController:TrackChargeCycle');
$this->get('/batteries/get-battery-details/{batteryId}', 'Grocy\Controllers\BatteriesApiController:BatteryDetails');
// Batteries
$this->get('/batteries/track-charge-cycle/{batteryId}', '\Grocy\Controllers\BatteriesApiController:TrackChargeCycle');
$this->get('/batteries/get-battery-details/{batteryId}', '\Grocy\Controllers\BatteriesApiController:BatteryDetails');
})->add(new ApiKeyAuthMiddleware($appContainer, $appContainer->LoginControllerInstance->GetSessionCookieName(), $appContainer->ApiKeyHeaderName))
->add(JsonMiddleware::class)
->add(new CorsMiddleware([
@@ -95,8 +107,3 @@ $app->group('/api', function()
'credentials' => false,
'cache' => 0,
]));
$app->group('/cli', function()
{
$this->get('/recreatedemo', 'Grocy\Controllers\CliController:RecreateDemo');
})->add(CliMiddleware::class);

View File

@@ -39,6 +39,7 @@ class ApiKeyService extends BaseService
$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
));
$apiKeyRow->save();
@@ -57,6 +58,16 @@ class ApiKeyService extends BaseService
return $apiKey->id;
}
public function GetUserByApiKey($apiKey)
{
$apiKeyRow = $this->Database->api_keys()->where('api_key', $apiKey)->fetch();
if ($apiKeyRow !== null)
{
return $this->Database->users($apiKeyRow->user_id);
}
return null;
}
private function GenerateApiKey()
{
return RandomString(50);

View File

@@ -4,22 +4,6 @@ namespace Grocy\Services;
class ApplicationService extends BaseService
{
/**
* @return boolean
*/
public function IsDemoInstallation()
{
return file_exists(DATAPATH . '/demo.txt');
}
/**
* @return boolean
*/
public function IsEmbeddedInstallation()
{
return file_exists(__DIR__ . '/../embedded.txt');
}
private $InstalledVersion;
public function GetInstalledVersion()
{

View File

@@ -11,7 +11,7 @@ class BaseService
$this->DatabaseService = new DatabaseService();
$this->Database = $this->DatabaseService->GetDbConnection();
$localizationService = new LocalizationService(CULTURE);
$localizationService = new LocalizationService(GROCY_CULTURE);
$this->LocalizationService = $localizationService;
}

View File

@@ -8,21 +8,38 @@ class DatabaseMigrationService extends BaseService
{
$this->DatabaseService->ExecuteDbStatement("CREATE TABLE IF NOT EXISTS migrations (migration INTEGER NOT NULL PRIMARY KEY UNIQUE, execution_time_timestamp DATETIME DEFAULT (datetime('now', 'localtime')))");
$migrationFiles = array();
$sqlMigrationFiles = array();
foreach (new \FilesystemIterator(__DIR__ . '/../migrations') as $file)
{
$migrationFiles[$file->getBasename('.sql')] = $file->getPathname();
if ($file->getExtension() === 'sql')
{
$sqlMigrationFiles[$file->getBasename('.sql')] = $file->getPathname();
}
}
ksort($migrationFiles);
foreach($migrationFiles as $migrationNumber => $migrationFile)
ksort($sqlMigrationFiles);
foreach($sqlMigrationFiles as $migrationNumber => $migrationFile)
{
$migrationNumber = ltrim($migrationNumber, '0');
$this->ExecuteMigrationWhenNeeded($migrationNumber, file_get_contents($migrationFile));
$this->ExecuteSqlMigrationWhenNeeded($migrationNumber, file_get_contents($migrationFile));
}
$phpMigrationFiles = array();
foreach (new \FilesystemIterator(__DIR__ . '/../migrations') as $file)
{
if ($file->getExtension() === 'php')
{
$phpMigrationFiles[$file->getBasename('.php')] = $file->getPathname();
}
}
ksort($phpMigrationFiles);
foreach($phpMigrationFiles as $migrationNumber => $migrationFile)
{
$migrationNumber = ltrim($migrationNumber, '0');
$this->ExecutePhpMigrationWhenNeeded($migrationNumber, $migrationFile);
}
}
private function ExecuteMigrationWhenNeeded(int $migrationId, string $sql)
private function ExecuteSqlMigrationWhenNeeded(int $migrationId, string $sql)
{
$rowCount = $this->DatabaseService->ExecuteDbQuery('SELECT COUNT(*) FROM migrations WHERE migration = ' . $migrationId)->fetchColumn();
if (intval($rowCount) === 0)
@@ -31,4 +48,14 @@ class DatabaseMigrationService extends BaseService
$this->DatabaseService->ExecuteDbStatement('INSERT INTO migrations (migration) VALUES (' . $migrationId . ')');
}
}
private function ExecutePhpMigrationWhenNeeded(int $migrationId, string $phpFile)
{
$rowCount = $this->DatabaseService->ExecuteDbQuery('SELECT COUNT(*) FROM migrations WHERE migration = ' . $migrationId)->fetchColumn();
if (intval($rowCount) === 0)
{
include $phpFile;
$this->DatabaseService->ExecuteDbStatement('INSERT INTO migrations (migration) VALUES (' . $migrationId . ')');
}
}
}

View File

@@ -14,7 +14,7 @@ class DatabaseService
{
if ($this->DbConnectionRaw == null)
{
$pdo = new \PDO('sqlite:' . DATAPATH . '/grocy.db');
$pdo = new \PDO('sqlite:' . GROCY_DATAPATH . '/grocy.db');
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->DbConnectionRaw = $pdo;
}

View File

@@ -8,7 +8,7 @@ class DemoDataGeneratorService extends BaseService
{
public function PopulateDemoData()
{
$localizationService = new LocalizationService(CULTURE);
$localizationService = new LocalizationService(GROCY_CULTURE);
$rowCount = $this->DatabaseService->ExecuteDbQuery('SELECT COUNT(*) FROM migrations WHERE migration = -1')->fetchColumn();
if (intval($rowCount) === 0)
@@ -16,6 +16,11 @@ class DemoDataGeneratorService extends BaseService
$loremIpsum = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.';
$sql = "
UPDATE users SET username = '{$localizationService->Localize('Demo User')}' WHERE id = 1;
INSERT INTO users (username, password) VALUES ('{$localizationService->Localize('Demo User')} 2', 'x');
INSERT INTO users (username, password) VALUES ('{$localizationService->Localize('Demo User')} 3', 'x');
INSERT INTO users (username, password) VALUES ('{$localizationService->Localize('Demo User')} 4', 'x');
INSERT INTO locations (name) VALUES ('{$localizationService->Localize('Pantry')}'); --2
INSERT INTO locations (name) VALUES ('{$localizationService->Localize('Candy cupboard')}'); --3
INSERT INTO locations (name) VALUES ('{$localizationService->Localize('Tinned food cupboard')}'); --4
@@ -123,7 +128,7 @@ class DemoDataGeneratorService extends BaseService
public function RecreateDemo()
{
unlink(DATAPATH . '/grocy.db');
unlink(GROCY_DATAPATH . '/grocy.db');
$this->PopulateDemoData();
}
}

View File

@@ -44,24 +44,40 @@ class HabitsService extends BaseService
$habit = $this->Database->habits($habitId);
$habitTrackedCount = $this->Database->habits_log()->where('habit_id', $habitId)->count();
$habitLastTrackedTime = $this->Database->habits_log()->where('habit_id', $habitId)->max('tracked_time');
$doneByUserId = $this->Database->habits_log()->where('habit_id = :1 AND tracked_time = :2', $habitId, $habitLastTrackedTime)->fetch()->done_by_user_id;
if ($doneByUserId !== null && !empty($doneByUserId))
{
$usersService = new UsersService();
$users = $usersService->GetUsersAsDto();
$lastDoneByUser = FindObjectInArrayByPropertyValue($users, 'id', $doneByUserId);
}
return array(
'habit' => $habit,
'last_tracked' => $habitLastTrackedTime,
'tracked_count' => $habitTrackedCount
'tracked_count' => $habitTrackedCount,
'last_done_by' => $lastDoneByUser
);
}
public function TrackHabit(int $habitId, string $trackedTime)
public function TrackHabit(int $habitId, string $trackedTime, $doneBy = GROCY_USER_ID)
{
if (!$this->HabitExists($habitId))
{
throw new \Exception('Habit does not exist');
}
$userRow = $this->Database->users()->where('id = :1', $doneBy)->fetch();
if ($userRow === null)
{
throw new \Exception('User does not exist');
}
$logRow = $this->Database->habits_log()->createRow(array(
'habit_id' => $habitId,
'tracked_time' => $trackedTime
'tracked_time' => $trackedTime,
'done_by_user_id' => $doneBy
));
$logRow->save();

View File

@@ -36,7 +36,7 @@ class LocalizationService
private function LogMissingLocalization(string $culture, string $text)
{
$file = DATAPATH . "/missing_translations_$culture.json";
$file = GROCY_DATAPATH . "/missing_translations_$culture.json";
$missingTranslations = array();
if (file_exists($file))
@@ -57,7 +57,7 @@ class LocalizationService
public function Localize(string $text, ...$placeholderValues)
{
if (MODE === 'dev')
if (GROCY_MODE === 'dev')
{
if (!array_key_exists($text, $this->StringsDefaultCulture))
{

View File

@@ -33,11 +33,12 @@ class SessionService extends BaseService
/**
* @return string
*/
public function CreateSession()
public function CreateSession($userId)
{
$newSessionKey = $this->GenerateSessionKey();
$sessionRow = $this->Database->sessions()->createRow(array(
'user_id' => $userId,
'session_key' => $newSessionKey,
'expires' => date('Y-m-d H:i:s', time() + 2592000) // Default is that sessions expire in 30 days
));
@@ -51,6 +52,21 @@ class SessionService extends BaseService
$this->Database->sessions()->where('session_key', $sessionKey)->delete();
}
public function GetUserBySessionKey($sessionKey)
{
$sessionRow = $this->Database->sessions()->where('session_key', $sessionKey)->fetch();
if ($sessionRow !== null)
{
return $this->Database->users($sessionRow->user_id);
}
return null;
}
public function GetDefaultUser()
{
return $this->Database->users(1);
}
private function GenerateSessionKey()
{
return RandomString(50);

View File

@@ -216,13 +216,13 @@ class StockService extends BaseService
private function LoadBarcodeLookupPlugin()
{
$pluginName = defined('STOCK_BARCODE_LOOKUP_PLUGIN') ? STOCK_BARCODE_LOOKUP_PLUGIN : '';
$pluginName = defined('GROCY_STOCK_BARCODE_LOOKUP_PLUGIN') ? GROCY_STOCK_BARCODE_LOOKUP_PLUGIN : '';
if (empty($pluginName))
{
throw new \Exception('No barcode lookup plugin defined');
}
$path = DATAPATH . "/plugins/$pluginName.php";
$path = GROCY_DATAPATH . "/plugins/$pluginName.php";
if (file_exists($path))
{
require_once $path;

58
services/UsersService.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
namespace Grocy\Services;
class UsersService extends BaseService
{
public function CreateUser(string $username, string $firstName, string $lastName, string $password)
{
$newUserRow = $this->Database->users()->createRow(array(
'username' => $username,
'first_name' => $firstName,
'last_name' => $lastName,
'password' => password_hash($password, PASSWORD_DEFAULT)
));
$newUserRow->save();
}
public function EditUser(int $userId, string $username, string $firstName, string $lastName, string $password)
{
if (!$this->UserExists($userId))
{
throw new \Exception('User does not exist');
}
$user = $this->Database->users($userId);
$user->update(array(
'username' => $username,
'first_name' => $firstName,
'last_name' => $lastName,
'password' => password_hash($password, PASSWORD_DEFAULT)
));
}
public function DeleteUser($userId)
{
$row = $this->Database->users($userId);
$row->delete();
}
public function GetUsersAsDto()
{
$users = $this->Database->users();
$returnUsers = array();
foreach ($users as $user)
{
unset($user->password);
$user->display_name = GetUserDisplayName($user);
$returnUsers[] = $user;
}
return $returnUsers;
}
private function UserExists($userId)
{
$userRow = $this->Database->users()->where('id = :1', $userId)->fetch();
return $userRow !== null;
}
}

34
update.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/bin/bash
GROCY_RELEASE_URL=https://releases.grocy.info/latest
echo Start updating grocy
set -e
shopt -s extglob
pushd `dirname $0` > /dev/null
backupBundleFileName="backup-`date +%d-%m-%Y-%H-%M-%S`.tgz"
echo Making a backup of the current installation in /data/backups/$backupBundleFileName
mkdir -p ./data/backups > /dev/null
tar -zcvf ./data/backups/$backupBundleFileName --exclude ./data/backups . > /dev/null
find ./data/backups/*.tgz -mtime +60 -type f -delete
echo Deleting everything except /data and this script
rm -rf !(data|update.sh) > /dev/null
echo Emptying /data/viewcache
rm -rf ./data/viewcache/* > /dev/null
echo Downloading latest release
rm -f ./grocy-latest.zip > /dev/null
wget $GROCY_RELEASE_URL -q -O ./grocy-latest.zip > /dev/null
echo Unzipping latest release
unzip -o ./grocy-latest.zip > /dev/null
rm -f ./grocy-latest.zip > /dev/null
popd > /dev/null
echo Finished updating grocy

View File

@@ -1,4 +1,4 @@
{
"Version": "1.15.0",
"ReleaseDate": "2018-07-22"
"Version": "1.16.0",
"ReleaseDate": "2018-07-25"
}

View File

@@ -37,7 +37,7 @@
</thead>
<tbody>
@foreach($current as $curentBatteryEntry)
<tr class="@if(FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->charge_interval_days > 0 && $nextChargeTimes[$curentBatteryEntry->battery_id] < date('Y-m-d H:i:s')) table-danger @endif">
<tr class="@if(FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->charge_interval_days > 0 && $nextChargeTimes[$curentBatteryEntry->battery_id] < date('Y-m-d H:i:s')) table-danger @elseif(FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->charge_interval_days > 0 && $nextChargeTimes[$curentBatteryEntry->battery_id] < date('Y-m-d H:i:s', strtotime('+5 days'))) table-warning @endif">
<td class="fit-content">
<a class="btn btn-success btn-sm track-charge-cycle-button" href="#" title="{{ $L('Track charge cycle of battery #1', FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->name) }}"
data-battery-id="{{ $curentBatteryEntry->battery_id }}"

View File

@@ -10,5 +10,6 @@
<h3><span id="habitcard-habit-name"></span></h3>
<strong>{{ $L('Tracked count') }}:</strong> <span id="habitcard-habit-tracked-count"></span><br>
<strong>{{ $L('Last tracked') }}:</strong> <span id="habitcard-habit-last-tracked"></span> <time id="habitcard-habit-last-tracked-timeago" class="timeago timeago-contextual"></time><br>
<strong>{{ $L('Last done by') }}:</strong> <span id="habitcard-habit-last-done-by"></span>
</div>
</div>

View File

@@ -7,7 +7,7 @@
<div class="form-group" data-next-input-selector="{{ $nextInputSelector }}" data-disallow-add-product-workflows="{{ BoolToString($disallowAddProductWorkflows) }}" data-prefill-by-name="{{ $prefillByName }}">
<label for="product_id">{{ $L('Product') }} <i class="fas fa-barcode"></i><span id="barcode-lookup-disabled-hint" class="small text-muted d-none"> {{ $L('Barcode lookup is disabled') }}</span></label>
<select class="form-control combobox" id="product_id" name="product_id" required>
<select class="form-control product-combobox" id="product_id" name="product_id" required>
<option value=""></option>
@foreach($products as $product)
<option data-additional-searchdata="{{ $product->barcode }}" value="{{ $product->id }}">{{ $product->name }}</option>

View File

@@ -0,0 +1,16 @@
@push('componentScripts')
<script src="{{ $U('/viewjs/components/userpicker.js', true) }}?v={{ $version }}"></script>
@endpush
@php if(empty($prefillByUsername)) { $prefillByUsername = ''; } @endphp
@php if(empty($prefillByUserId)) { $prefillByUserId = ''; } @endphp
<div class="form-group" data-next-input-selector="{{ $nextInputSelector }}" data-prefill-by-username="{{ $prefillByUsername }}" data-prefill-by-user-id="{{ $prefillByUserId }}">
<label for="user_id">{{ $L($label) }}</label>
<select class="form-control user-combobox" id="user_id" name="user_id">
<option value=""></option>
@foreach($users as $user)
<option data-additional-searchdata="{{ $user->username }}" value="{{ $user->id }}">{{ GetUserDisplayName($user) }}</option>
@endforeach
</select>
</div>

View File

@@ -0,0 +1,63 @@
@extends('layout.default')
@section('title', $L('Habits analysis'))
@section('activeNav', 'habitsanalysis')
@section('viewJsName', 'habitsanalysis')
@section('content')
<div class="row">
<div class="col">
<h1>@yield('title')</h1>
</div>
</div>
<div class="row mt-3">
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="habit-filter">{{ $L('Filter by habit') }}</label> <i class="fas fa-filter"></i>
<select class="form-control" id="habit-filter">
<option value="all">{{ $L('All') }}</option>
@foreach($habits as $habit)
<option value="{{ $habit->id }}">{{ $habit->name }}</option>
@endforeach
</select>
</div>
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="search">{{ $L('Search') }}</label> <i class="fas fa-search"></i>
<input type="text" class="form-control" id="search">
</div>
</div>
<div class="row">
<div class="col">
<table id="habits-analysis-table" class="table table-sm table-striped dt-responsive">
<thead>
<tr>
<th>{{ $L('Habit') }}</th>
<th>{{ $L('Tracked time') }}</th>
<th>{{ $L('Done by') }}</th>
</tr>
</thead>
<tbody>
@foreach($habitsLog as $habitLogEntry)
<tr>
<td>
{{ FindObjectInArrayByPropertyValue($habits, 'id', $habitLogEntry->habit_id)->name }}
</td>
<td>
{{ $habitLogEntry->tracked_time }}
<time class="timeago timeago-contextual" datetime="{{ $habitLogEntry->tracked_time }}"></time>
</td>
<td>
@if ($habitLogEntry->done_by_user_id !== null && !empty($habitLogEntry->done_by_user_id))
{{ GetUserDisplayName(FindObjectInArrayByPropertyValue($users, 'id', $habitLogEntry->done_by_user_id)) }}
@else
{{ $L('Unknown') }}
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@stop

View File

@@ -37,13 +37,16 @@
</thead>
<tbody>
@foreach($currentHabits as $curentHabitEntry)
<tr class="@if(FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->period_type === \Grocy\Services\HabitsService::HABIT_TYPE_DYNAMIC_REGULAR && $nextHabitTimes[$curentHabitEntry->habit_id] < date('Y-m-d H:i:s')) table-danger @endif">
<tr class="@if(FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->period_type === \Grocy\Services\HabitsService::HABIT_TYPE_DYNAMIC_REGULAR && $nextHabitTimes[$curentHabitEntry->habit_id] < date('Y-m-d H:i:s')) table-danger @elseif(FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->period_type === \Grocy\Services\HabitsService::HABIT_TYPE_DYNAMIC_REGULAR && $nextHabitTimes[$curentHabitEntry->habit_id] < date('Y-m-d H:i:s', strtotime('+5 days'))) table-warning @endif">
<td class="fit-content">
<a class="btn btn-success btn-sm track-habit-button" href="#" title="{{ $L('Track execution of habit #1', FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->name) }}"
data-habit-id="{{ $curentHabitEntry->habit_id }}"
data-habit-name="{{ FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->name }}">
<i class="fas fa-play"></i>
</a>
<a class="btn btn-info btn-sm" href="{{ $U('/habitsanalysis?habit=') }}{{ $curentHabitEntry->habit_id }}">
<i class="fas fa-chart-line"></i>
</a>
</td>
<td>
{{ FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->name }}

View File

@@ -32,6 +32,13 @@
'invalidFeedback' => $L('This can only be before now')
))
@include('components.userpicker', array(
'label' => 'Done by',
'users' => $users,
'nextInputSelector' => '#user_id',
'prefillByUserId' => GROCY_USER_ID
))
<button id="save-habittracking-button" type="submit" class="btn btn-success">{{ $L('OK') }}</button>
</form>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{ CULTURE }}">
<html lang="{{ GROCY_CULTURE }}">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
@@ -28,8 +28,8 @@
<link href="{{ $U('/css/grocy.css?v=', true) }}{{ $version }}" rel="stylesheet">
@stack('pageStyles')
@if(file_exists(DATAPATH . '/custom_css.html'))
@php include DATAPATH . '/custom_css.html' @endphp
@if(file_exists(GROCY_DATAPATH . '/custom_css.html'))
@php include GROCY_DATAPATH . '/custom_css.html' @endphp
@endif
<script>
@@ -38,7 +38,7 @@
Grocy.BaseUrl = '{{ $U('/') }}';
Grocy.LocalizationStrings = {!! json_encode($localizationStrings) !!};
Grocy.ActiveNav = '@yield('activeNav', '')';
Grocy.Culture = '{{ CULTURE }}';
Grocy.Culture = '{{ GROCY_CULTURE }}';
</script>
</head>
@@ -164,12 +164,14 @@
</ul>
<ul class="navbar-nav ml-auto">
@if(AUTHENTICATED === true && $isEmbeddedInstallation === false)
@if(GROCY_AUTHENTICATED === true && !GROCY_IS_EMBEDDED_INSTALL)
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle discrete-link" href="#" data-toggle="dropdown"><i class="fas fa-user"></i> {{ HTTP_USER }}</a>
<a class="nav-link dropdown-toggle discrete-link" href="#" data-toggle="dropdown"><i class="fas fa-user"></i> {{ GROCY_USER_USERNAME }}</a>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item logout-button discrete-link" href="{{ $U('/logout') }}"><i class="fas fa-sign-out-alt"></i>&nbsp;{{ $L('Logout') }}</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item logout-button discrete-link" href="{{ $U('/user/' . GROCY_USER_ID . '?changepw=true') }}"><i class="fas fa-key"></i>&nbsp;{{ $L('Change password') }}</a>
</div>
</li>
@endif
@@ -178,11 +180,11 @@
<a class="nav-link dropdown-toggle discrete-link" href="#" data-toggle="dropdown"><i class="fas fa-wrench"></i> <span class="d-inline d-lg-none">{{ $L('Settings') }}</span></a>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item discrete-link" href="{{ $U('/users') }}"><i class="fas fa-users"></i>&nbsp;{{ $L('Manage users') }}</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item discrete-link" href="{{ $U('/manageapikeys') }}"><i class="fas fa-handshake"></i>&nbsp;{{ $L('Manage API keys') }}</a>
<a class="dropdown-item discrete-link" target="_blank" href="{{ $U('/api') }}"><i class="fas fa-book"></i>&nbsp;{{ $L('REST API & data model documentation') }}</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item discrete-link" href="#" data-toggle="modal" data-target="#about-modal"><i class="fas fa-info fa-fw"></i>&nbsp;{{ $L('About grocy') }} (Version {{ $version }})</a>
</div>
</li>
@@ -252,8 +254,8 @@
@stack('componentScripts')
<script src="{{ $U('/viewjs', true) }}/@yield('viewJsName').js?v={{ $version }}"></script>
@if(file_exists(DATAPATH . '/custom_js.html'))
@php include DATAPATH . '/custom_js.html' @endphp
@if(file_exists(GROCY_DATAPATH . '/custom_js.html'))
@php include GROCY_DATAPATH . '/custom_js.html' @endphp
@endif
</body>

View File

@@ -34,6 +34,7 @@
<tr>
<th>#</th>
<th>{{ $L('API key') }}</th>
<th>{{ $L('User') }}</th>
<th>{{ $L('Expires') }}</th>
<th>{{ $L('Last used') }}</th>
<th>{{ $L('Created') }}</th>
@@ -50,6 +51,9 @@
<td>
{{ $apiKey->api_key }}
</td>
<td>
{{ GetUserDisplayName(FindObjectInArrayByPropertyValue($users, 'id', $apiKey->user_id)) }}
</td>
<td>
{{ $apiKey->expires }}
<time class="timeago timeago-contextual" datetime="{{ $apiKey->expires }}"></time>

View File

@@ -30,8 +30,8 @@
<script src="{{ $U('/node_modules/swagger-ui-dist/swagger-ui-standalone-preset.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/viewjs', true) }}/openapiui.js?v={{ $version }}"></script>
@if(file_exists(DATAPATH . '/add_before_end_body.html'))
@php include DATAPATH . '/add_before_end_body.html' @endphp
@if(file_exists(GROCY_DATAPATH . '/add_before_end_body.html'))
@php include GROCY_DATAPATH . '/add_before_end_body.html' @endphp
@endif
</body>
</html>

56
views/userform.blade.php Normal file
View File

@@ -0,0 +1,56 @@
@extends('layout.default')
@if($mode == 'edit')
@section('title', $L('Edit user'))
@else
@section('title', $L('Create user'))
@endif
@section('viewJsName', 'userform')
@section('content')
<div class="row">
<div class="col-lg-6 col-xs-12">
<h1>@yield('title')</h1>
<script>Grocy.EditMode = '{{ $mode }}';</script>
@if($mode == 'edit')
<script>Grocy.EditObjectId = {{ $user->id }};</script>
@endif
<form id="user-form" novalidate>
<div class="form-group">
<label for="username">{{ $L('Username') }}</label>
<input type="text" class="form-control" required id="username" name="username" value="@if($mode == 'edit'){{ $user->username }}@endif">
<div class="invalid-feedback">{{ $L('A username is required') }}</div>
</div>
<div class="form-group">
<label for="first_name">{{ $L('First name') }}</label>
<input type="text" class="form-control" id="first_name" name="first_name" value="@if($mode == 'edit'){{ $user->first_name }}@endif">
</div>
<div class="form-group">
<label for="last_name">{{ $L('Last name') }}</label>
<input type="text" class="form-control" id="last_name" name="last_name" value="@if($mode == 'edit'){{ $user->last_name }}@endif">
</div>
<div class="form-group">
<label for="password">{{ $L('Password') }}</label>
<input type="password" class="form-control" required id="password" name="password">
</div>
<div class="form-group">
<label for="password_confirm">{{ $L('Confirm password') }}</label>
<input type="password" class="form-control" required id="password_confirm" name="password_confirm">
<div class="invalid-feedback">{{ $L('Passwords do not match') }}</div>
</div>
<button id="save-user-button" type="submit" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>
</div>
@stop

63
views/users.blade.php Normal file
View File

@@ -0,0 +1,63 @@
@extends('layout.default')
@section('title', $L('Users'))
@section('activeNav', '')
@section('viewJsName', 'users')
@section('content')
<div class="row">
<div class="col">
<h1>
@yield('title')
<a class="btn btn-outline-dark" href="{{ $U('/user/new') }}">
<i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
</h1>
</div>
</div>
<div class="row mt-3">
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="search">{{ $L('Search') }}</label> <i class="fas fa-search"></i>
<input type="text" class="form-control" id="search">
</div>
</div>
<div class="row">
<div class="col">
<table id="users-table" class="table table-sm table-striped dt-responsive">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Username') }}</th>
<th>{{ $L('First name') }}</th>
<th>{{ $L('Last name') }}</th>
</tr>
</thead>
<tbody>
@foreach($users as $user)
<tr>
<td class="fit-content">
<a class="btn btn-info btn-sm" href="{{ $U('/user/') }}{{ $user->id }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-danger btn-sm user-delete-button @if($user->id == GROCY_USER_ID) disabled @endif" href="#" data-user-id="{{ $user->id }}" data-user-username="{{ $user->username }}">
<i class="fas fa-trash"></i>
</a>
</td>
<td>
{{ $user->username }}
</td>
<td>
{{ $user->first_name }}
</td>
<td>
{{ $user->last_name }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@stop

View File

@@ -7,8 +7,8 @@
resolved "https://github.com/pallidus-fintech/bootstrap-combobox.git#0bd1da781b99d390f1c75315b6025e7d8658b263"
"@fortawesome/fontawesome-free@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.1.0.tgz#f35f5ba91366b7a58b0b6a4f22ff0907fe002219"
version "5.2.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.2.0.tgz#50cd9856774351c56c0b1b0db4efe122d7913e58"
"TagManager@https://github.com/max-favilli/tagmanager.git#3.0.2":
version "3.0.1"
@@ -29,8 +29,8 @@ bootstrap@4.0.0:
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.0.0.tgz#ceb03842c145fcc1b9b4e15da2a05656ba68469a"
bootstrap@^4.1.1:
version "4.1.2"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.2.tgz#aee2a93472e61c471fc79fb475531dcbc87de326"
version "4.1.3"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.3.tgz#0eb371af2c8448e8c210411d0cb824a6409a12be"
chart.js@2.7.1:
version "2.7.1"
@@ -185,8 +185,8 @@ startbootstrap-sb-admin@^4.0.0:
jquery.easing "^1.4.1"
swagger-ui-dist@^3.17.3:
version "3.17.4"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.17.4.tgz#7b4d3842b052cbadebec784265b2e17fdda6a232"
version "3.17.5"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.17.5.tgz#ccab9dc35d16a3d244d26b5975e36a684211665a"
tempusdominus-bootstrap-4@^5.0.1:
version "5.0.1"