mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-07-07 20:06:36 +02:00
Enhancement - #363 export.ical
- Added feature #363 supporting iCalendar export via the 'export.ical' command. - Updated documentation. - Removed unnecessary localization of canonical command names.
This commit is contained in:
parent
3ef6aa9f8e
commit
e368043fb8
15 changed files with 288 additions and 90 deletions
|
@ -8,6 +8,8 @@
|
|||
by using the 'log' command in place of 'add' (thanks to Cory Donnelly).
|
||||
+ Added features #36 and #37, providing annual versions of the 'history'
|
||||
and 'ghistory' command as 'history.annual' and 'ghistory.annual'.
|
||||
+ Added feature #363 supporting iCalendar export via the 'export.ical'
|
||||
command.
|
||||
+ Fixed bug #406 so that task now includes command aliases in the _commands
|
||||
helper command used by shell completion scripts.
|
||||
|
||||
|
|
1
NEWS
1
NEWS
|
@ -18,6 +18,7 @@ New Features in task 1.9
|
|||
- New 'log' command to add tasks that are already completed.
|
||||
- New annual history and ghistory command variations.
|
||||
- Alias support in shell completion scripts.
|
||||
- New iCalendar export format.
|
||||
- Task is now part of Debian
|
||||
|
||||
Please refer to the ChangeLog file for full details. There are too many to
|
||||
|
|
|
@ -133,8 +133,14 @@ Shows task database statistics.
|
|||
Imports tasks from a variety of formats.
|
||||
|
||||
.TP
|
||||
.B export \fIfile
|
||||
Exports all tasks as a CSV file.
|
||||
.B export
|
||||
Exports all tasks in CSV format. This command is an alias to the export.csv command.
|
||||
Redirect the output to a file, if you wish to save it, or pipe it to another command.
|
||||
|
||||
.TP
|
||||
.B export.ical
|
||||
Exports all tasks in iCalendar format.
|
||||
Redirect the output to a file, if you wish to save it, or pipe it to another command.
|
||||
|
||||
.TP
|
||||
.B color [sample]
|
||||
|
|
|
@ -273,7 +273,8 @@ way.
|
|||
.B alias.rm=delete
|
||||
Task supports command aliases. This alias provides an alternate name (rm) for
|
||||
the delete command. You can use aliases to provide alternate names for any of
|
||||
task's commands.
|
||||
task's commands. Several commands you may use are actually aliases - 'history',
|
||||
for example, or 'export'.
|
||||
|
||||
.SS DATES
|
||||
|
||||
|
|
|
@ -40,10 +40,9 @@
|
|||
208 done
|
||||
209 duplicate
|
||||
210 edit
|
||||
211 export
|
||||
|
||||
212 help
|
||||
213 history.monthly
|
||||
214 ghistory.monthly
|
||||
|
||||
215 import
|
||||
216 info
|
||||
217 prepend
|
||||
|
@ -60,8 +59,6 @@
|
|||
228 version
|
||||
229 shell
|
||||
230 config
|
||||
231 history.annual
|
||||
232 ghistory.annual
|
||||
|
||||
# 3xx Attributes - must be sequential
|
||||
300 project
|
||||
|
|
25
src/Cmd.cpp
25
src/Cmd.cpp
|
@ -105,12 +105,21 @@ void Cmd::load ()
|
|||
{
|
||||
if (commands.size () == 0)
|
||||
{
|
||||
// Commands whose names are not localized.
|
||||
commands.push_back ("_projects");
|
||||
commands.push_back ("_tags");
|
||||
commands.push_back ("_commands");
|
||||
commands.push_back ("_ids");
|
||||
commands.push_back ("_config");
|
||||
commands.push_back ("_version");
|
||||
commands.push_back ("export.csv");
|
||||
commands.push_back ("export.ical");
|
||||
commands.push_back ("history.monthly");
|
||||
commands.push_back ("history.annual");
|
||||
commands.push_back ("ghistory.monthly");
|
||||
commands.push_back ("ghistory.annual");
|
||||
|
||||
// Commands whose names are localized.
|
||||
commands.push_back (context.stringtable.get (CMD_ADD, "add"));
|
||||
commands.push_back (context.stringtable.get (CMD_APPEND, "append"));
|
||||
commands.push_back (context.stringtable.get (CMD_ANNOTATE, "annotate"));
|
||||
|
@ -121,12 +130,7 @@ void Cmd::load ()
|
|||
commands.push_back (context.stringtable.get (CMD_DONE, "done"));
|
||||
commands.push_back (context.stringtable.get (CMD_DUPLICATE, "duplicate"));
|
||||
commands.push_back (context.stringtable.get (CMD_EDIT, "edit"));
|
||||
commands.push_back (context.stringtable.get (CMD_EXPORT, "export"));
|
||||
commands.push_back (context.stringtable.get (CMD_HELP, "help"));
|
||||
commands.push_back (context.stringtable.get (CMD_HISTORY_MONTHLY, "history.monthly"));
|
||||
commands.push_back (context.stringtable.get (CMD_HISTORY_ANNUAL, "history.annual"));
|
||||
commands.push_back (context.stringtable.get (CMD_GHISTORY_MONTHLY, "ghistory.monthly"));
|
||||
commands.push_back (context.stringtable.get (CMD_GHISTORY_ANNUAL, "ghistory.annual"));
|
||||
commands.push_back (context.stringtable.get (CMD_IMPORT, "import"));
|
||||
commands.push_back (context.stringtable.get (CMD_INFO, "info"));
|
||||
commands.push_back (context.stringtable.get (CMD_LOG, "log"));
|
||||
|
@ -198,15 +202,16 @@ bool Cmd::isReadOnlyCommand ()
|
|||
command == "_ids" ||
|
||||
command == "_config" ||
|
||||
command == "_version" ||
|
||||
command == "export.csv" ||
|
||||
command == "export.ical" ||
|
||||
command == "history.monthly" ||
|
||||
command == "history.annual" ||
|
||||
command == "ghistory.monthly" ||
|
||||
command == "ghistory.annual" ||
|
||||
command == context.stringtable.get (CMD_CALENDAR, "calendar") ||
|
||||
command == context.stringtable.get (CMD_COLORS, "colors") ||
|
||||
command == context.stringtable.get (CMD_CONFIG, "config") ||
|
||||
command == context.stringtable.get (CMD_EXPORT, "export") ||
|
||||
command == context.stringtable.get (CMD_HELP, "help") ||
|
||||
command == context.stringtable.get (CMD_HISTORY_MONTHLY, "history.monthly") ||
|
||||
command == context.stringtable.get (CMD_HISTORY_ANNUAL, "history.annual") ||
|
||||
command == context.stringtable.get (CMD_GHISTORY_MONTHLY, "ghistory.monthly") ||
|
||||
command == context.stringtable.get (CMD_GHISTORY_ANNUAL, "ghistory.annual") ||
|
||||
command == context.stringtable.get (CMD_INFO, "info") ||
|
||||
command == context.stringtable.get (CMD_PROJECTS, "projects") ||
|
||||
command == context.stringtable.get (CMD_SHELL, "shell") ||
|
||||
|
|
|
@ -159,10 +159,14 @@ std::string Config::defaults =
|
|||
"#import.synonym.tags=?\n"
|
||||
"#import.synonym.uuid=?\n"
|
||||
"\n"
|
||||
"# Export Controls\n"
|
||||
"export.ical.class=PRIVATE # Could be PUBLIC, PRIVATE or CONFIDENTIAL\n"
|
||||
"\n"
|
||||
"# Aliases - alternate names for commands\n"
|
||||
"alias.rm=delete # Alias for the delete command\n"
|
||||
"alias.history=history.monthly # Prefer monthly history reports\n"
|
||||
"alias.ghistory=ghistory.monthly # Prefer monthly graphical history reports\n"
|
||||
"alias.history=history.monthly # Prefer monthly over annual history reports\n"
|
||||
"alias.ghistory=ghistory.monthly # Prefer monthly graphical over annual history reports\n"
|
||||
"alias.export=export.csv # Prefer CSV over iCal export\n"
|
||||
"\n"
|
||||
"# Fields: id,uuid,project,priority,priority_long,entry,entry_time,\n" // TODO
|
||||
"# start,entry_time,due,recur,recurrence_indicator,age,\n" // TODO
|
||||
|
|
|
@ -229,7 +229,8 @@ int Context::dispatch (std::string &out)
|
|||
else if (cmd.command == "delete") { rc = handleDelete (out); }
|
||||
else if (cmd.command == "start") { rc = handleStart (out); }
|
||||
else if (cmd.command == "stop") { rc = handleStop (out); }
|
||||
else if (cmd.command == "export") { rc = handleExport (out); }
|
||||
else if (cmd.command == "export.csv") { rc = handleExportCSV (out); }
|
||||
else if (cmd.command == "export.ical") { rc = handleExportiCal (out); }
|
||||
else if (cmd.command == "import") { rc = handleImport (out); }
|
||||
else if (cmd.command == "duplicate") { rc = handleDuplicate (out); }
|
||||
else if (cmd.command == "edit") { rc = handleEdit (out); }
|
||||
|
|
|
@ -5,12 +5,12 @@ task_SOURCES = API.cpp Att.cpp Cmd.cpp Color.cpp Config.cpp Context.cpp \
|
|||
Grid.cpp Hooks.cpp Keymap.cpp Location.cpp Nibbler.cpp \
|
||||
Path.cpp Permission.cpp Record.cpp Sequence.cpp \
|
||||
StringTable.cpp Subst.cpp TDB.cpp Table.cpp Task.cpp Timer.cpp \
|
||||
command.cpp custom.cpp edit.cpp import.cpp interactive.cpp \
|
||||
main.cpp recur.cpp report.cpp rules.cpp text.cpp util.cpp \
|
||||
API.h Att.h Cmd.h Color.h Config.h Context.h Date.h \
|
||||
Directory.h Duration.h File.h Filter.h Grid.h Hooks.h Keymap.h \
|
||||
Location.h Nibbler.h Path.h Permission.h Record.h Sequence.h \
|
||||
StringTable.h Subst.h TDB.h Table.h Task.h Timer.h i18n.h \
|
||||
main.h text.h util.h
|
||||
command.cpp custom.cpp edit.cpp export.cpp import.cpp \
|
||||
interactive.cpp main.cpp recur.cpp report.cpp rules.cpp \
|
||||
text.cpp util.cpp API.h Att.h Cmd.h Color.h Config.h Context.h \
|
||||
Date.h Directory.h Duration.h File.h Filter.h Grid.h Hooks.h \
|
||||
Keymap.h Location.h Nibbler.h Path.h Permission.h Record.h \
|
||||
Sequence.h StringTable.h Subst.h TDB.h Table.h Task.h Timer.h \
|
||||
i18n.h main.h text.h util.h
|
||||
task_CPPFLAGS=$(LUA_CFLAGS)
|
||||
task_LDFLAGS=$(LUA_LFLAGS)
|
||||
|
|
|
@ -1306,59 +1306,6 @@ int handleDone (std::string &outs)
|
|||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int handleExport (std::string &outs)
|
||||
{
|
||||
int rc = 0;
|
||||
|
||||
if (context.hooks.trigger ("pre-export-command"))
|
||||
{
|
||||
std::stringstream out;
|
||||
|
||||
// Deliberately no 'id'.
|
||||
out << "'uuid',"
|
||||
<< "'status',"
|
||||
<< "'tags',"
|
||||
<< "'entry',"
|
||||
<< "'start',"
|
||||
<< "'due',"
|
||||
<< "'recur',"
|
||||
<< "'end',"
|
||||
<< "'project',"
|
||||
<< "'priority',"
|
||||
<< "'fg',"
|
||||
<< "'bg',"
|
||||
<< "'description'"
|
||||
<< "\n";
|
||||
|
||||
int count = 0;
|
||||
|
||||
// Get all the tasks.
|
||||
std::vector <Task> tasks;
|
||||
context.tdb.lock (context.config.getBoolean ("locking"));
|
||||
handleRecurrence ();
|
||||
context.tdb.load (tasks, context.filter);
|
||||
context.tdb.commit ();
|
||||
context.tdb.unlock ();
|
||||
|
||||
foreach (task, tasks)
|
||||
{
|
||||
context.hooks.trigger ("pre-display", *task);
|
||||
|
||||
if (task->getStatus () != Task::recurring)
|
||||
{
|
||||
out << task->composeCSV ().c_str ();
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
outs = out.str ();
|
||||
context.hooks.trigger ("post-export-command");
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int handleModify (std::string &outs)
|
||||
{
|
||||
|
|
229
src/export.cpp
Normal file
229
src/export.cpp
Normal file
|
@ -0,0 +1,229 @@
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
// task - a command line task list manager.
|
||||
//
|
||||
// Copyright 2006 - 2010, Paul Beckingham, Federico Hernandez.
|
||||
// All rights reserved.
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or modify it under
|
||||
// the terms of the GNU General Public License as published by the Free Software
|
||||
// Foundation; either version 2 of the License, or (at your option) any later
|
||||
// version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
// details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program; if not, write to the
|
||||
//
|
||||
// Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor,
|
||||
// Boston, MA
|
||||
// 02110-1301
|
||||
// USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
#include "Att.h"
|
||||
#include "text.h"
|
||||
#include "util.h"
|
||||
#include "main.h"
|
||||
#include "../auto.h"
|
||||
|
||||
extern Context context;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int handleExportCSV (std::string &outs)
|
||||
{
|
||||
int rc = 0;
|
||||
|
||||
if (context.hooks.trigger ("pre-export-command"))
|
||||
{
|
||||
std::stringstream out;
|
||||
|
||||
// Deliberately no 'id'.
|
||||
out << "'uuid',"
|
||||
<< "'status',"
|
||||
<< "'tags',"
|
||||
<< "'entry',"
|
||||
<< "'start',"
|
||||
<< "'due',"
|
||||
<< "'recur',"
|
||||
<< "'end',"
|
||||
<< "'project',"
|
||||
<< "'priority',"
|
||||
<< "'fg',"
|
||||
<< "'bg',"
|
||||
<< "'description'"
|
||||
<< "\n";
|
||||
|
||||
int count = 0;
|
||||
|
||||
// Get all the tasks.
|
||||
std::vector <Task> tasks;
|
||||
context.tdb.lock (context.config.getBoolean ("locking"));
|
||||
handleRecurrence ();
|
||||
context.tdb.load (tasks, context.filter);
|
||||
context.tdb.commit ();
|
||||
context.tdb.unlock ();
|
||||
|
||||
foreach (task, tasks)
|
||||
{
|
||||
context.hooks.trigger ("pre-display", *task);
|
||||
|
||||
if (task->getStatus () != Task::recurring)
|
||||
{
|
||||
out << task->composeCSV ().c_str ();
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
outs = out.str ();
|
||||
context.hooks.trigger ("post-export-command");
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// http://tools.ietf.org/html/rfc5545
|
||||
//
|
||||
// Note: Recurring tasks could be included in more detail.
|
||||
int handleExportiCal (std::string &outs)
|
||||
{
|
||||
int rc = 0;
|
||||
|
||||
if (context.hooks.trigger ("pre-export-command"))
|
||||
{
|
||||
std::stringstream out;
|
||||
|
||||
out << "BEGIN:VCALENDAR\n"
|
||||
<< "VERSION:2.0\n"
|
||||
<< "PRODID:-//GBF//" << PACKAGE_STRING << "//EN\n";
|
||||
|
||||
int count = 0;
|
||||
|
||||
// Get all the tasks.
|
||||
std::vector <Task> tasks;
|
||||
context.tdb.lock (context.config.getBoolean ("locking"));
|
||||
handleRecurrence ();
|
||||
context.tdb.load (tasks, context.filter);
|
||||
context.tdb.commit ();
|
||||
context.tdb.unlock ();
|
||||
|
||||
foreach (task, tasks)
|
||||
{
|
||||
context.hooks.trigger ("pre-display", *task);
|
||||
|
||||
if (task->getStatus () != Task::recurring)
|
||||
{
|
||||
out << "BEGIN:VTODO\n";
|
||||
|
||||
// Required UID:20070313T123432Z-456553@example.com
|
||||
out << "UID:" << task->get ("uuid") << "\n";
|
||||
|
||||
// Required DTSTAMP:20070313T123432Z
|
||||
Date entry (atoi (task->get ("entry").c_str ()));
|
||||
out << "DTSTAMP:" << entry.toISO () << "\n";
|
||||
|
||||
// Optional DTSTART:20070514T110000Z
|
||||
if (task->has ("start"))
|
||||
{
|
||||
Date start (atoi (task->get ("start").c_str ()));
|
||||
out << "DTSTART:" << start.toISO () << "\n";
|
||||
}
|
||||
|
||||
// Optional DUE:20070709T130000Z
|
||||
if (task->has ("due"))
|
||||
{
|
||||
Date due (atoi (task->get ("due").c_str ()));
|
||||
out << "DUE:" << due.toISO () << "\n";
|
||||
}
|
||||
|
||||
// Optional COMPLETED:20070707T100000Z
|
||||
if (task->has ("end") && task->getStatus () == Task::completed)
|
||||
{
|
||||
Date end (atoi (task->get ("end").c_str ()));
|
||||
out << "COMPLETED:" << end.toISO () << "\n";
|
||||
}
|
||||
|
||||
out << "SUMMARY:" << task->get ("description") << "\n";
|
||||
|
||||
// Optional CLASS:PUBLIC/PRIVATE/CONFIDENTIAL
|
||||
std::string classification = context.config.get ("export.ical.class");
|
||||
if (classification == "")
|
||||
classification = "PRIVATE";
|
||||
out << "CLASS:" << classification << "\n";
|
||||
|
||||
// Optional multiple CATEGORIES:FAMILY,FINANCE
|
||||
if (task->getTagCount () > 0)
|
||||
{
|
||||
std::vector <std::string> tags;
|
||||
task->getTags (tags);
|
||||
std::string all;
|
||||
join (all, ",", tags);
|
||||
out << "CATEGORIES:" << all << "\n";
|
||||
}
|
||||
|
||||
// Optional PRIORITY:
|
||||
// 1-4 H
|
||||
// 5 M
|
||||
// 6-9 L
|
||||
if (task->has ("priority"))
|
||||
{
|
||||
out << "PRIORITY:";
|
||||
std::string priority = task->get ("priority");
|
||||
|
||||
if (priority == "H") out << "1";
|
||||
else if (priority == "M") out << "5";
|
||||
else out << "9";
|
||||
|
||||
out << "\n";
|
||||
}
|
||||
|
||||
// Optional STATUS:NEEDS-ACTION/IN-PROCESS/COMPLETED/CANCELLED
|
||||
out << "STATUS:";
|
||||
Task::status stat = task->getStatus ();
|
||||
if (stat == Task::pending || stat == Task::waiting)
|
||||
{
|
||||
if (task->has ("start"))
|
||||
out << "IN-PROCESS";
|
||||
else
|
||||
out << "NEEDS-ACTION";
|
||||
}
|
||||
else if (stat == Task::completed)
|
||||
{
|
||||
out << "COMPLETED";
|
||||
}
|
||||
else if (stat == Task::deleted)
|
||||
{
|
||||
out << "CANCELLED";
|
||||
}
|
||||
out << "\n";
|
||||
|
||||
// Optional COMMENT:annotation1
|
||||
// Optional COMMENT:annotation2
|
||||
std::vector <Att> annotations;
|
||||
task->getAnnotations (annotations);
|
||||
foreach (anno, annotations)
|
||||
out << "COMMENT:" << anno->value () << "\n";
|
||||
|
||||
out << "END:VTODO\n";
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
out << "END:VCALENDAR\n";
|
||||
|
||||
outs = out.str ();
|
||||
context.hooks.trigger ("post-export-command");
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
|
@ -77,10 +77,9 @@
|
|||
#define CMD_DONE 208
|
||||
#define CMD_DUPLICATE 209
|
||||
#define CMD_EDIT 210
|
||||
#define CMD_EXPORT 211
|
||||
|
||||
#define CMD_HELP 212
|
||||
#define CMD_HISTORY_MONTHLY 213
|
||||
#define CMD_GHISTORY_MONTHLY 214
|
||||
|
||||
#define CMD_IMPORT 215
|
||||
#define CMD_INFO 216
|
||||
#define CMD_PREPEND 217
|
||||
|
@ -97,8 +96,6 @@
|
|||
#define CMD_VERSION 228
|
||||
#define CMD_SHELL 229
|
||||
#define CMD_CONFIG 230
|
||||
#define CMD_HISTORY_ANNUAL 231
|
||||
#define CMD_GHISTORY_ANNUAL 232
|
||||
|
||||
// 3xx Attributes
|
||||
#define ATT_PROJECT 300
|
||||
|
|
|
@ -59,7 +59,6 @@ int handleAdd (std::string &);
|
|||
int handleLog (std::string &);
|
||||
int handleAppend (std::string &);
|
||||
int handlePrepend (std::string &);
|
||||
int handleExport (std::string &);
|
||||
int handleDone (std::string &);
|
||||
int handleModify (std::string &);
|
||||
int handleProjects (std::string &);
|
||||
|
@ -127,6 +126,10 @@ std::string colorizeDebug (const std::string&);
|
|||
// import.cpp
|
||||
int handleImport (std::string&);
|
||||
|
||||
// export.cpp
|
||||
int handleExportCSV (std::string &);
|
||||
int handleExportiCal (std::string &);
|
||||
|
||||
// list template
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
template <class T> bool listDiff (const T& left, const T& right)
|
||||
|
|
|
@ -192,7 +192,11 @@ int shortUsage (std::string &outs)
|
|||
|
||||
row = table.addRow ();
|
||||
table.addCell (row, 1, "task export");
|
||||
table.addCell (row, 2, "Lists all tasks in CSV format.");
|
||||
table.addCell (row, 2, "Lists all tasks in CSV format. Alias to export.csv");
|
||||
|
||||
row = table.addRow ();
|
||||
table.addCell (row, 1, "task export.ical");
|
||||
table.addCell (row, 2, "Lists all tasks in iCalendar format.");
|
||||
|
||||
row = table.addRow ();
|
||||
table.addCell (row, 1, "task color [sample]");
|
||||
|
|
|
@ -9,8 +9,9 @@ OBJECTS = ../t-TDB.o ../t-Task.o ../t-text.o ../t-Date.o ../t-Table.o \
|
|||
../t-Nibbler.o ../t-Location.o ../t-Filter.o ../t-Context.o \
|
||||
../t-Keymap.o ../t-command.o ../t-interactive.o ../t-report.o \
|
||||
../t-Grid.o ../t-Color.o ../t-rules.o ../t-recur.o ../t-custom.o \
|
||||
../t-import.o ../t-edit.o ../t-Timer.o ../t-Permission.o ../t-Path.o \
|
||||
../t-File.o ../t-Directory.o ../t-Hooks.o ../t-API.o
|
||||
../t-export.o ../t-import.o ../t-edit.o ../t-Timer.o \
|
||||
../t-Permission.o ../t-Path.o ../t-File.o ../t-Directory.o \
|
||||
../t-Hooks.o ../t-API.o
|
||||
|
||||
all: $(PROJECT)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue