Commit 94a05562 authored by Thomas Löffler's avatar Thomas Löffler
Browse files

Merge branch '44-add-export-option-for-memberships' into 'main'

Resolve "Add memberships statistic"

Closes #44

See merge request !16
parents 02460b3f 2797b48c
Pipeline #15130 passed with stage
in 7 seconds
<?php
namespace T3o\T3oMembership\Command;
use Dropbox\Dropbox;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class ImportMembersCommand extends \Symfony\Component\Console\Command\Command
{
/**
* @var array
*/
protected $hookObjects = [];
/**
* @var array
*/
protected $memberships = [];
/**
* Configure the command by defining the name, options and arguments
*/
protected function configure(): void
{
$this->setDescription('Import members');
$this->addArgument('importFile', InputArgument::REQUIRED);
$this->addArgument('membershipStoragePid', InputArgument::REQUIRED);
$this->addArgument('dropboxToken', InputArgument::REQUIRED);
}
/**
* Executes the command
*
* @param InputInterface $input
* @param OutputInterface $output
*/
protected function execute(InputInterface $input, OutputInterface $output): void
{
$io = new SymfonyStyle($input, $output);
$io->title($this->getDescription());
/** @var $logger \TYPO3\CMS\Core\Log\Logger */
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
$logger->debug('Execute');
$membershipRecords = $this->getDatabaseConnection()
->getConnectionForTable('tx_t3omembership_domain_model_membership')
->select(['uid', 'name'], 'tx_t3omembership_domain_model_membership')
->fetchAll();
foreach ($membershipRecords as $membershipRecord) {
$this->memberships[$membershipRecord['name']] = (int)$membershipRecord['uid'];
}
// does the import file exist?
$importFile = $this->getImportFile($input);
if (!GeneralUtility::isAbsPath($importFile)) {
$importFile = GeneralUtility::getFileAbsFileName($importFile);
}
if (!file_exists($importFile)) {
$logger->debug('No importfile', ['filename' => $importFile]);
$io->writeln('No importfile found');
return;
}
$this->initializeHookObjects();
$fileData = file($importFile);
array_shift($fileData);
foreach ($fileData as $key => $line) {
$line = iconv('ISO-8859-15', 'UTF-8', $line);
/** @noinspection PhpParamsInspection */
$fields = GeneralUtility::trimExplode("\t", $line);
$membershipUid = $this->getMembershipUid($fields[12]);
// Skip records with unknown membership types.
if (empty($membershipUid)) {
continue;
}
$subscriptionNo = (int)$fields[14];
$endDate = $this->getMemberEndDate($fields[15]);
// If the user has cancelled his membership "Gekündigt", we set the endtime enable field.
$endTime = !empty($fields[17]) ? $endDate : 0;
$hidden = false;
if ($endTime > 0 && $endTime < time()) {
$hidden = true;
}
$member = [
'name' => $fields[6],
'subscription_no' => $subscriptionNo,
'external_id' => (int)$fields[0],
'address' => $fields[7] !== '' ? $fields[7] : $fields[8],
'zip' => $fields[10],
'city' => $fields[11],
'country' => $fields[13],
'end_date' => $endDate,
'endtime' => $endTime,
'hidden' => (int)$hidden,
'starttime' => 0,
'membership' => $membershipUid,
'pid' => $input->getArgument('membershipStoragePid'),
'crdate' => time(),
'tstamp' => time(),
'invoice_email' => $fields[84],
'email' => $fields[79],
'url' => $fields[80],
'firstname' => $fields[82],
'lastname' => $fields[83]
];
$memberUid = $this->createOrUpdateMember($subscriptionNo, $member);
foreach ($this->hookObjects as $hookObject) {
if (method_exists($hookObject, 'postUpdateMemberData')) {
$hookObject->postUpdateMemberData($memberUid, $member);
}
}
}
$io->writeln('Finished.');
}
protected function getDatabaseConnection(): ConnectionPool
{
return GeneralUtility::makeInstance(ConnectionPool::class);
}
/**
* Gets importFile
*
* @return string
*/
public function getImportFile(InputInterface $input): string
{
$dropbox = new Dropbox($input->getArgument('dropboxToken'));
$dropbox->files->download('/Member-Lists/memberlist.txt', $input->getArgument('importFile'));
return $input->getArgument('importFile');
}
/**
* Instantiates all configured hook objects.
*/
protected function initializeHookObjects(): void
{
if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3omembership']['importMemberTaksHooks'])) {
return;
}
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3omembership']['importMemberTaksHooks'] as $classData) {
$hookObject = GeneralUtility::makeInstance($classData);
if (!is_object($hookObject)) {
throw new \UnexpectedValueException( // @TODO Namespace?
'The hook object class ' . $classData . ' could not be instantiated.');
}
$this->hookObjects[] = $hookObject;
}
}
protected function getMembershipUid(string $membershipName): ?int
{
$membershipName = trim(str_replace('Membership', '', $membershipName));
if (empty($this->memberships[$membershipName])) {
return null;
}
return $this->memberships[$membershipName];
}
/**
* Parses the value of the "Beginn" column to a UNIX timestamp.
*
* @param string $endDate
* @return int
*/
protected function getMemberEndDate(string $endDate): int
{
if (empty($endDate)) {
return 0;
}
$endDateTime = \DateTime::createFromFormat('d.m.Y', $endDate);
$endDateTime->setTime(0, 0, 0);
return $endDateTime->getTimestamp();
}
/**
* Checks if the member with the given subscription number already exists in the database.
* If he exists, his data will be updated, otherwise a new record will be inserted.
*
* @param int $subscriptionNo
* @param array $memberData
* @return int The uid of the updated / inserted member.
*/
protected function createOrUpdateMember(int $subscriptionNo, array $memberData): int
{
$queryBuilder = $this->getDatabaseConnection()->getQueryBuilderForTable('tx_t3omembership_domain_model_member');
// remove hidden restriction to find former member
$queryBuilder
->getRestrictions()
->removeByType(\TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction::class)
->removeByType(\TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction::class);
$statement = $queryBuilder->select('uid')->from('tx_t3omembership_domain_model_member')->where($queryBuilder->expr()
->eq('subscription_no', $queryBuilder->createNamedParameter($subscriptionNo, \PDO::PARAM_INT)))->execute();
$existingMember = $statement->fetch();
if (!empty($existingMember['uid'])) {
$memberUid = $existingMember['uid'];
$this->getDatabaseConnection()
->getConnectionForTable('tx_t3omembership_domain_model_member')
->update('tx_t3omembership_domain_model_member', $memberData, ['uid' => (int)$memberUid]);
} else {
$datebaseConnection = $this->getDatabaseConnection()->getConnectionForTable('tx_t3omembership_domain_model_member');
$datebaseConnection->insert('tx_t3omembership_domain_model_member', $memberData);
$memberUid = (int)$datebaseConnection->lastInsertId('tx_t3omembership_domain_model_member');
}
return (int)$memberUid;
}
}
<?php
namespace T3o\T3oMembership\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class MemberstatisticCommand extends Command
{
/**
* @var array
*/
protected $memberships = [];
/**
* Configure the command by defining the name, options and arguments
*/
protected function configure(): void
{
$this->setDescription('Update membership statistic');
$this->addArgument('membershipStoragePid', InputArgument::REQUIRED);
}
/**
* Executes the command
*
* @param InputInterface $input
* @param OutputInterface $output
*/
protected function execute(InputInterface $input, OutputInterface $output): void
{
$io = new SymfonyStyle($input, $output);
$io->title($this->getDescription());
/** @var $logger \TYPO3\CMS\Core\Log\Logger */
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
$logger->debug('Execute');
if ($this->createStatistic((int)$input->getArgument('membershipStoragePid'))) {
$io->writeln('Statistic is updated');
$logger->debug('updating statistics on PID: ' . $input->getArgument('membershipStoragePid'));
} else {
$io->writeln('Error on updating statistics');
}
}
protected function getDatabaseConnection(): ConnectionPool
{
return GeneralUtility::makeInstance(ConnectionPool::class);
}
protected function createOrUpdateMemberStatistic(int $membership, int $pid ): bool
{
$queryBuilder = $this->getDatabaseConnection()->getQueryBuilderForTable('tx_t3omembership_domain_model_member');
$statement = $queryBuilder->count('*')->from('tx_t3omembership_domain_model_member')->where($queryBuilder->expr()
->eq('membership', $membership))->execute();
$count = $statement->fetchColumn();
$memberData = array();
$memberData['year'] = date('Y');;
$memberData['month'] = date('m');;
$memberData['membership'] = $membership;
$memberData['count'] = $count;
$memberData['pid'] = $pid;
$queryBuilder = $this->getDatabaseConnection()->getQueryBuilderForTable('tx_t3omembership_domain_model_member_statistic');
$statement = $queryBuilder->select('uid')->from('tx_t3omembership_domain_model_member_statistic')
->where(
$queryBuilder->expr()->eq('year', $memberData['year']),
$queryBuilder->expr()->eq('month', $memberData['month']),
$queryBuilder->expr()->eq('membership', $membership)
)
->execute();
$existingData = $statement->fetch();
if (!empty($existingData['uid'])) {
$memberStatisticUid = $existingData['uid'];
if ($this->getDatabaseConnection()
->getConnectionForTable('tx_t3omembership_domain_model_member_statistic')
->update('tx_t3omembership_domain_model_member_statistic', $memberData,
['uid' => (int)$memberStatisticUid])) {
return true;
}
} else {
$datebaseConnection = $this->getDatabaseConnection()->getConnectionForTable('tx_t3omembership_domain_model_member_statistic');
if ($datebaseConnection->insert('tx_t3omembership_domain_model_member_statistic', $memberData)) {
return true;
}
}
return false;
}
protected function createStatistic(int $pid): bool
{
// get all membershiptypes
$queryBuilder = $this->getDatabaseConnection()->getQueryBuilderForTable('tx_t3omembership_domain_model_member');
$statement = $queryBuilder->select('uid')->from('tx_t3omembership_domain_model_membership')->execute();
$memberships = $statement->fetchAll();
$result = true;
foreach ($memberships as $membership) {
if ($this->createOrUpdateMemberStatistic((int)$membership['uid'], (int)$pid) === false) {
$result = false;
}
}
return $result;
}
}
<?php
return [
't3omembership:importmembers' => [
'class' => \T3o\T3oMembership\Command\ImportMembersCommand::class,
'schedulable' => true,
],
];
\ No newline at end of file
services:
_defaults:
autowire: true
autoconfigure: true
public: false
T3o\T3oMembership\:
resource: '../Classes/*'
T3o\T3oMembership\Command\MemberstatisticCommand:
tags:
- name: 'console.command'
command: 't3omembership:updateStatistic'
schedulable: true
<?php
if (!defined('TYPO3_MODE')) {
die ('Access denied.');
}
return [
'ctrl' => [
'title' => 'LLL:EXT:t3o_membership/Resources/Private/Language/locallang_db.xlf:tx_t3omembership_domain_model_member_statistic',
'label' => 'Memberstatistic',
'label_alt' => 'year,month,membership',
'label_alt_force' => true,
'tstamp' => 'tstamp',
'crdate' => 'crdate',
'cruser_id' => 'cruser_id',
'dividers2tabs' => true,
'delete' => 'deleted',
'enablecolumns' => [
'disabled' => 'hidden',
'starttime' => 'starttime',
'endtime' => 'endtime',
],
'searchFields' => 'year, month, membership',
'iconfile' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('t3o_membership') . 'Resources/Public/Icons/tx_t3omembership_domain_model_member.gif'
],
'interface' => [
'showRecordFieldList' => 'year, month, membership, count',
],
'types' => [
'1' => ['showitem' => 'year, month, membership, count, --div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.access,starttime, endtime'],
],
'palettes' => [
'1' => ['showitem' => ''],
],
'columns' => [
'hidden' => [
'exclude' => true,
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.hidden',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'default' => 0,
'items' => [
[
0 => '',
1 => '',
]
],
]
],
'starttime' => [
'exclude' => true,
'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:starttime_formlabel',
'config' => [
'type' => 'input',
'renderType' => 'inputDateTime',
'size' => 16,
'eval' => 'datetime,int',
'default' => 0,
'behaviour' => [
'allowLanguageSynchronization' => true,
],
]
],
'endtime' => [
'exclude' => true,
'label' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:endtime_formlabel',
'config' => [
'type' => 'input',
'renderType' => 'inputDateTime',
'size' => 16,
'eval' => 'datetime,int',
'default' => 0,
'behaviour' => [
'allowLanguageSynchronization' => true,
],
]
],
'month' => [
'exclude' => 0,
'label' => 'LLL:EXT:t3o_membership/Resources/Private/Language/locallang_db.xlf:tx_t3omembership_domain_model_member_statistic.month',
'config' => [
'type' => 'input',
'size' => 2,
'eval' => 'int, required',
'readOnly' => true
],
],
'year' => [
'exclude' => 0,
'label' => 'LLL:EXT:t3o_membership/Resources/Private/Language/locallang_db.xlf:tx_t3omembership_domain_model_member_statistic.year',
'config' => [
'type' => 'input',
'size' => 4,
'eval' => 'int,required',
'readOnly' => true
],
],
'count' => [
'exclude' => 0,
'label' => 'LLL:EXT:t3o_membership/Resources/Private/Language/locallang_db.xlf:tx_t3omembership_domain_model_member_statistic.count',
'config' => [
'type' => 'input',
'size' => 10,
'eval' => 'int,required',
'readOnly' => true
],
],
'membership' => [
'exclude' => 0,
'label' => 'LLL:EXT:t3o_membership/Resources/Private/Language/locallang_db.xlf:tx_t3omembership_domain_model_member.membership',
'config' => [
'type' => 'select',
'renderType' => 'selectSingle',
'foreign_table' => 'tx_t3omembership_domain_model_membership',
'minitems' => 0,
'maxitems' => 1,
'readOnly' => true
],
],
],
];
# t3o membership
## What does it do?
This extension imports our members from dropbox and displays at the
This extension imports our members from dropbox and displays at the
memberlisting at the Assocation page.
## Import Settings
......@@ -11,15 +11,17 @@ Installation:
* Set up a scheduler task of type membership import
* set up:
* storage pid: Where should the members be imported
* CSV file to import (path and filename): Full path to the location where the memeber file should be stored. The folder should be secured via .htaccess
* Dropbox Token: Token for accessing the dropbox folder
* Setup the my.typo3.org API token
The expected location within dropbox is:
/TYPO3 Backoffice/Member-Lists/memberlist.txt
The file is endocded with: ISO-8859-15
## Membership Statistic
## External libs
You can add a scheduler task, to create a monthly statistic. All
data are written to the table "!tx_t3omembership_domain_model_member_statistic"
and can be viewed via TYPO3 Backend. The data are written to the given
PID in the Memberstatistic Command.
This extension uses Dropbox-v2-PHP-SDK from lukeb2014, see:
https://github.com/lukeb2014/Dropbox-v2-PHP-SDK
Configuration:
* Set up a scheduler task of type update membershipstatistic
* Provide the PID, where the membership data are stored
* the Task can be run once per month, as the data of each run are updated to the corresponding month.
......@@ -78,6 +78,9 @@
<trans-unit id="tx_t3omembership_task_importmemberstask.description">
<source>Import members from CSV file</source>
</trans-unit>
<trans-unit id="tx_t3omembership_domain_model_member_statistic">
<source>Member Statistic</source>
</trans-unit>
</body>
</file>
</xliff>
\ No newline at end of file
</xliff>
......@@ -23,8 +23,7 @@
"issues": "https://git-t3o.typo3.org/t3o/t3o_membership/issues"
},
"require": {
"typo3/cms-core": "10.3.0 - 10.9.99",
"lukebaird/dropbox-v2-php-sdk": "^1.1"
"typo3/cms-core": "10.3.0 - 10.9.99"
},
"autoload": {
"psr-4": {
......
......@@ -27,7 +27,7 @@ $EM_CONF['t3o_membership'] = [
'modify_tables' => '',
'clearCacheOnLoad' => 0,
'lockType' => '',
'version' => '3.0.7',
'version' => '3.9.0',
'constraints' => [
'depends' => [
'typo3' => '10.4.0'
......
......@@ -86,3 +86,10 @@ CREATE TABLE tx_t3omembership_domain_model_membership (
KEY language (l10n_parent,sys_language_uid)
);
CREATE TABLE tx_t3omembership_domain_model_member_statistic (