mirror of
https://github.com/RetroShare/RetroShare.git
synced 2025-06-09 23:22:48 -04:00
Lots of progress with Gxs Services:
- Added gxsForum interface, service + serialiser to libretroshare. - Bugfix in rsgenservices getSize() at the wrong point, Added lots of debug too. - Dummy Collections to Wiki, can now create and retrieve Groups from the GUI. - Bugfix in rsinit (wrong backend for wiki) + added forum to startup. - improved debugging in GxsId serialiser. git-svn-id: http://svn.code.sf.net/p/retroshare/code/branches/v0.5-gxs-b1@5797 b45a01b8-16f6-495d-af2f-9b41ad6348cc
This commit is contained in:
parent
8170697029
commit
eeb96c5e62
13 changed files with 1214 additions and 19 deletions
369
libretroshare/src/services/p3gxsforums.cc
Normal file
369
libretroshare/src/services/p3gxsforums.cc
Normal file
|
@ -0,0 +1,369 @@
|
|||
/*
|
||||
* libretroshare/src/services p3gxsforums.cc
|
||||
*
|
||||
* GxsForums interface for RetroShare.
|
||||
*
|
||||
* Copyright 2012-2012 by Robert Fernie.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License Version 2.1 as published by the Free Software Foundation.
|
||||
*
|
||||
* This library 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
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
|
||||
* USA.
|
||||
*
|
||||
* Please report all bugs and problems to "retroshare@lunamutt.com".
|
||||
*
|
||||
*/
|
||||
|
||||
#include "services/p3gxsforums.h"
|
||||
#include "serialiser/rsgxsforumitems.h"
|
||||
|
||||
#include "util/rsrandom.h"
|
||||
#include <stdio.h>
|
||||
|
||||
/****
|
||||
* #define GXSFORUM_DEBUG 1
|
||||
****/
|
||||
|
||||
RsGxsForums *rsGxsForums = NULL;
|
||||
|
||||
|
||||
|
||||
/********************************************************************************/
|
||||
/******************* Startup / Tick ******************************************/
|
||||
/********************************************************************************/
|
||||
|
||||
p3GxsForums::p3GxsForums(RsGeneralDataService *gds, RsNetworkExchangeService *nes)
|
||||
: RsGenExchange(gds, nes, new RsGxsForumSerialiser(), RS_SERVICE_GXSV1_TYPE_FORUMS), RsGxsForums(this)
|
||||
{
|
||||
}
|
||||
|
||||
void p3GxsForums::notifyChanges(std::vector<RsGxsNotify *> &changes)
|
||||
{
|
||||
receiveChanges(changes);
|
||||
}
|
||||
|
||||
void p3GxsForums::service_tick()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool p3GxsForums::getGroupData(const uint32_t &token, std::vector<RsGxsForumGroup> &groups)
|
||||
{
|
||||
std::vector<RsGxsGrpItem*> grpData;
|
||||
bool ok = RsGenExchange::getGroupData(token, grpData);
|
||||
|
||||
if(ok)
|
||||
{
|
||||
std::vector<RsGxsGrpItem*>::iterator vit = grpData.begin();
|
||||
|
||||
for(; vit != grpData.end(); vit++)
|
||||
{
|
||||
RsGxsForumGroupItem* item = dynamic_cast<RsGxsForumGroupItem*>(*vit);
|
||||
RsGxsForumGroup grp = item->mGroup;
|
||||
item->mGroup.mMeta = item->meta;
|
||||
grp.mMeta = item->mGroup.mMeta;
|
||||
delete item;
|
||||
groups.push_back(grp);
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* Okay - chris is not going to be happy with this...
|
||||
* but I can't be bothered with crazy data structures
|
||||
* at the moment - fix it up later
|
||||
*/
|
||||
|
||||
bool p3GxsForums::getMsgData(const uint32_t &token, std::vector<RsGxsForumMsg> &msgs)
|
||||
{
|
||||
GxsMsgDataMap msgData;
|
||||
bool ok = RsGenExchange::getMsgData(token, msgData);
|
||||
|
||||
if(ok)
|
||||
{
|
||||
GxsMsgDataMap::iterator mit = msgData.begin();
|
||||
|
||||
for(; mit != msgData.end(); mit++)
|
||||
{
|
||||
RsGxsGroupId grpId = mit->first;
|
||||
std::vector<RsGxsMsgItem*>& msgItems = mit->second;
|
||||
std::vector<RsGxsMsgItem*>::iterator vit = msgItems.begin();
|
||||
|
||||
for(; vit != msgItems.end(); vit++)
|
||||
{
|
||||
RsGxsForumMsgItem* item = dynamic_cast<RsGxsForumMsgItem*>(*vit);
|
||||
|
||||
if(item)
|
||||
{
|
||||
RsGxsForumMsg msg = item->mMsg;
|
||||
msg.mMeta = item->meta;
|
||||
msgs.push_back(msg);
|
||||
delete item;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Not a GxsForumMsgItem, deleting!" << std::endl;
|
||||
delete *vit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
/********************************************************************************************/
|
||||
|
||||
bool p3GxsForums::createGroup(uint32_t &token, RsGxsForumGroup &group)
|
||||
{
|
||||
std::cerr << "p3GxsForums::createGroup()" << std::endl;
|
||||
|
||||
RsGxsForumGroupItem* grpItem = new RsGxsForumGroupItem();
|
||||
grpItem->mGroup = group;
|
||||
grpItem->meta = group.mMeta;
|
||||
|
||||
RsGenExchange::publishGroup(token, grpItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool p3GxsForums::createMsg(uint32_t &token, RsGxsForumMsg &msg)
|
||||
{
|
||||
std::cerr << "p3GxsForums::createForumMsg() GroupId: " << msg.mMeta.mGroupId;
|
||||
std::cerr << std::endl;
|
||||
|
||||
RsGxsForumMsgItem* msgItem = new RsGxsForumMsgItem();
|
||||
msgItem->mMsg = msg;
|
||||
msgItem->meta = msg.mMeta;
|
||||
|
||||
RsGenExchange::publishMsg(token, msgItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/********************************************************************************************/
|
||||
|
||||
|
||||
std::string p3GxsForums::genRandomId()
|
||||
{
|
||||
std::string randomId;
|
||||
for(int i = 0; i < 20; i++)
|
||||
{
|
||||
randomId += (char) ('a' + (RSRandom::random_u32() % 26));
|
||||
}
|
||||
|
||||
return randomId;
|
||||
}
|
||||
|
||||
bool p3GxsForums::generateDummyData()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
#if 0
|
||||
|
||||
bool p3GxsForums::generateDummyData()
|
||||
{
|
||||
/* so we want to generate 100's of forums */
|
||||
#define MAX_FORUMS 10 //100
|
||||
#define MAX_THREADS 10 //1000
|
||||
#define MAX_MSGS 100 //10000
|
||||
|
||||
std::list<RsForumV2Group> mGroups;
|
||||
std::list<RsForumV2Group>::iterator git;
|
||||
|
||||
std::list<RsForumV2Msg> mMsgs;
|
||||
std::list<RsForumV2Msg>::iterator mit;
|
||||
|
||||
#define DUMMY_NAME_MAX_LEN 10000
|
||||
char name[DUMMY_NAME_MAX_LEN];
|
||||
int i, j;
|
||||
time_t now = time(NULL);
|
||||
|
||||
for(i = 0; i < MAX_FORUMS; i++)
|
||||
{
|
||||
/* generate a new forum */
|
||||
RsForumV2Group forum;
|
||||
|
||||
/* generate a temp id */
|
||||
forum.mMeta.mGroupId = genRandomId();
|
||||
|
||||
snprintf(name, DUMMY_NAME_MAX_LEN, "TestForum_%d", i+1);
|
||||
|
||||
forum.mMeta.mGroupId = genRandomId();
|
||||
forum.mMeta.mGroupName = name;
|
||||
|
||||
forum.mMeta.mPublishTs = now - (RSRandom::random_f32() * 100000);
|
||||
/* key fields to fill in:
|
||||
* GroupId.
|
||||
* Name.
|
||||
* Flags.
|
||||
* Pop.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/* use probability to decide which are subscribed / own / popularity.
|
||||
*/
|
||||
|
||||
float rnd = RSRandom::random_f32();
|
||||
if (rnd < 0.1)
|
||||
{
|
||||
forum.mMeta.mSubscribeFlags = RSGXS_GROUP_SUBSCRIBE_ADMIN;
|
||||
|
||||
}
|
||||
else if (rnd < 0.3)
|
||||
{
|
||||
forum.mMeta.mSubscribeFlags = RSGXS_GROUP_SUBSCRIBE_SUBSCRIBED;
|
||||
}
|
||||
else
|
||||
{
|
||||
forum.mMeta.mSubscribeFlags = 0;
|
||||
}
|
||||
|
||||
forum.mMeta.mPop = (int) (RSRandom::random_f32() * 10.0);
|
||||
|
||||
mGroups.push_back(forum);
|
||||
|
||||
|
||||
//std::cerr << "p3GxsForums::generateDummyData() Generated Forum: " << forum.mMeta;
|
||||
//std::cerr << std::endl;
|
||||
}
|
||||
|
||||
|
||||
for(i = 0; i < MAX_THREADS; i++)
|
||||
{
|
||||
/* generate a base thread */
|
||||
|
||||
/* rotate the Forum Groups Around, then pick one.
|
||||
*/
|
||||
|
||||
int rnd = (int) (RSRandom::random_f32() * 10.0);
|
||||
|
||||
for(j = 0; j < rnd; j++)
|
||||
{
|
||||
RsForumV2Group head = mGroups.front();
|
||||
mGroups.pop_front();
|
||||
mGroups.push_back(head);
|
||||
}
|
||||
|
||||
RsForumV2Group forum = mGroups.front();
|
||||
|
||||
/* now create a new thread */
|
||||
|
||||
RsForumV2Msg msg;
|
||||
|
||||
/* fill in key data
|
||||
* GroupId
|
||||
* MsgId
|
||||
* OrigMsgId
|
||||
* ThreadId
|
||||
* ParentId
|
||||
* PublishTS (take Forum TS + a bit ).
|
||||
*
|
||||
* ChildTS ????
|
||||
*/
|
||||
snprintf(name, DUMMY_NAME_MAX_LEN, "%s => ThreadMsg_%d", forum.mMeta.mGroupName.c_str(), i+1);
|
||||
msg.mMeta.mMsgName = name;
|
||||
|
||||
msg.mMeta.mGroupId = forum.mMeta.mGroupId;
|
||||
msg.mMeta.mMsgId = genRandomId();
|
||||
msg.mMeta.mOrigMsgId = msg.mMeta.mMsgId;
|
||||
msg.mMeta.mThreadId = msg.mMeta.mMsgId;
|
||||
msg.mMeta.mParentId = "";
|
||||
|
||||
msg.mMeta.mPublishTs = forum.mMeta.mPublishTs + (RSRandom::random_f32() * 10000);
|
||||
if (msg.mMeta.mPublishTs > now)
|
||||
msg.mMeta.mPublishTs = now - 1;
|
||||
|
||||
mMsgs.push_back(msg);
|
||||
|
||||
//std::cerr << "p3GxsForums::generateDummyData() Generated Thread: " << msg.mMeta;
|
||||
//std::cerr << std::endl;
|
||||
|
||||
}
|
||||
|
||||
for(i = 0; i < MAX_MSGS; i++)
|
||||
{
|
||||
/* generate a base thread */
|
||||
|
||||
/* rotate the Forum Groups Around, then pick one.
|
||||
*/
|
||||
|
||||
int rnd = (int) (RSRandom::random_f32() * 10.0);
|
||||
|
||||
for(j = 0; j < rnd; j++)
|
||||
{
|
||||
RsForumV2Msg head = mMsgs.front();
|
||||
mMsgs.pop_front();
|
||||
mMsgs.push_back(head);
|
||||
}
|
||||
|
||||
RsForumV2Msg parent = mMsgs.front();
|
||||
|
||||
/* now create a new child msg */
|
||||
|
||||
RsForumV2Msg msg;
|
||||
|
||||
/* fill in key data
|
||||
* GroupId
|
||||
* MsgId
|
||||
* OrigMsgId
|
||||
* ThreadId
|
||||
* ParentId
|
||||
* PublishTS (take Forum TS + a bit ).
|
||||
*
|
||||
* ChildTS ????
|
||||
*/
|
||||
snprintf(name, DUMMY_NAME_MAX_LEN, "%s => Msg_%d", parent.mMeta.mMsgName.c_str(), i+1);
|
||||
msg.mMeta.mMsgName = name;
|
||||
msg.mMsg = name;
|
||||
|
||||
msg.mMeta.mGroupId = parent.mMeta.mGroupId;
|
||||
msg.mMeta.mMsgId = genRandomId();
|
||||
msg.mMeta.mOrigMsgId = msg.mMeta.mMsgId;
|
||||
msg.mMeta.mThreadId = parent.mMeta.mThreadId;
|
||||
msg.mMeta.mParentId = parent.mMeta.mOrigMsgId;
|
||||
|
||||
msg.mMeta.mPublishTs = parent.mMeta.mPublishTs + (RSRandom::random_f32() * 10000);
|
||||
if (msg.mMeta.mPublishTs > now)
|
||||
msg.mMeta.mPublishTs = now - 1;
|
||||
|
||||
mMsgs.push_back(msg);
|
||||
|
||||
//std::cerr << "p3GxsForums::generateDummyData() Generated Child Msg: " << msg.mMeta;
|
||||
//std::cerr << std::endl;
|
||||
|
||||
}
|
||||
|
||||
|
||||
mUpdated = true;
|
||||
|
||||
/* Then - at the end, we push them all into the Proxy */
|
||||
for(git = mGroups.begin(); git != mGroups.end(); git++)
|
||||
{
|
||||
/* pushback */
|
||||
mForumProxy->addForumGroup(*git);
|
||||
|
||||
}
|
||||
|
||||
for(mit = mMsgs.begin(); mit != mMsgs.end(); mit++)
|
||||
{
|
||||
/* pushback */
|
||||
mForumProxy->addForumMsg(*mit);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
73
libretroshare/src/services/p3gxsforums.h
Normal file
73
libretroshare/src/services/p3gxsforums.h
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* libretroshare/src/services: p3gxsforums.h
|
||||
*
|
||||
* GxsForum interface for RetroShare.
|
||||
*
|
||||
* Copyright 2012-2012 by Robert Fernie.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License Version 2 as published by the Free Software Foundation.
|
||||
*
|
||||
* This library 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
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
|
||||
* USA.
|
||||
*
|
||||
* Please report all bugs and problems to "retroshare@lunamutt.com".
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef P3_GXSFORUMS_SERVICE_HEADER
|
||||
#define P3_GXSFORUMS_SERVICE_HEADER
|
||||
|
||||
|
||||
#include "retroshare/rsgxsforums.h"
|
||||
#include "gxs/rsgenexchange.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
/*
|
||||
*
|
||||
*/
|
||||
|
||||
class p3GxsForums: public RsGenExchange, public RsGxsForums
|
||||
{
|
||||
public:
|
||||
|
||||
p3GxsForums(RsGeneralDataService* gds, RsNetworkExchangeService* nes);
|
||||
|
||||
protected:
|
||||
|
||||
virtual void notifyChanges(std::vector<RsGxsNotify*>& changes);
|
||||
virtual void service_tick();
|
||||
|
||||
public:
|
||||
|
||||
virtual bool getGroupData(const uint32_t &token, std::vector<RsGxsForumGroup> &groups);
|
||||
virtual bool getMsgData(const uint32_t &token, std::vector<RsGxsForumMsg> &msgs);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//virtual bool setMessageStatus(const std::string &msgId, const uint32_t status, const uint32_t statusMask);
|
||||
//virtual bool setGroupSubscribeFlags(const std::string &groupId, uint32_t subscribeFlags, uint32_t subscribeMask);
|
||||
|
||||
//virtual bool groupRestoreKeys(const std::string &groupId);
|
||||
//virtual bool groupShareKeys(const std::string &groupId, std::list<std::string>& peers);
|
||||
|
||||
virtual bool createGroup(uint32_t &token, RsGxsForumGroup &group);
|
||||
virtual bool createMsg(uint32_t &token, RsGxsForumMsg &msg);
|
||||
|
||||
private:
|
||||
|
||||
std::string genRandomId();
|
||||
bool generateDummyData();
|
||||
|
||||
};
|
||||
|
||||
#endif
|
|
@ -26,6 +26,8 @@
|
|||
#include "services/p3wiki.h"
|
||||
#include "serialiser/rswikiitems.h"
|
||||
|
||||
#include "util/rsrandom.h"
|
||||
|
||||
/****
|
||||
* #define WIKI_DEBUG 1
|
||||
****/
|
||||
|
@ -48,12 +50,18 @@ void p3Wiki::service_tick()
|
|||
|
||||
void p3Wiki::notifyChanges(std::vector<RsGxsNotify*>& changes)
|
||||
{
|
||||
std::cerr << "p3Wiki::notifyChanges() New stuff";
|
||||
std::cerr << std::endl;
|
||||
|
||||
receiveChanges(changes);
|
||||
}
|
||||
|
||||
/* Specific Service Data */
|
||||
bool p3Wiki::getCollections(const uint32_t &token, std::vector<RsWikiCollection> &collections)
|
||||
{
|
||||
std::cerr << "p3Wiki::getCollections()";
|
||||
std::cerr << std::endl;
|
||||
|
||||
std::vector<RsGxsGrpItem*> grpData;
|
||||
bool ok = RsGenExchange::getGroupData(token, grpData);
|
||||
|
||||
|
@ -64,10 +72,25 @@ bool p3Wiki::getCollections(const uint32_t &token, std::vector<RsWikiCollection>
|
|||
for(; vit != grpData.end(); vit++)
|
||||
{
|
||||
RsGxsWikiCollectionItem* item = dynamic_cast<RsGxsWikiCollectionItem*>(*vit);
|
||||
RsWikiCollection collection = item->collection;
|
||||
collection.mMeta = item->collection.mMeta;
|
||||
delete item;
|
||||
collections.push_back(collection);
|
||||
|
||||
if (item)
|
||||
{
|
||||
RsWikiCollection collection = item->collection;
|
||||
collection.mMeta = item->meta;
|
||||
delete item;
|
||||
collections.push_back(collection);
|
||||
|
||||
std::cerr << "p3Wiki::getCollections() Adding Collection to Vector: ";
|
||||
std::cerr << std::endl;
|
||||
std::cerr << collection;
|
||||
std::cerr << std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << "Not a WikiCollectionItem, deleting!" << std::endl;
|
||||
delete *vit;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
|
@ -159,6 +182,15 @@ bool p3Wiki::submitCollection(uint32_t &token, RsWikiCollection &collection)
|
|||
RsGxsWikiCollectionItem* collectionItem = new RsGxsWikiCollectionItem();
|
||||
collectionItem->collection = collection;
|
||||
collectionItem->meta = collection.mMeta;
|
||||
|
||||
std::cerr << "p3Wiki::submitCollection(): ";
|
||||
std::cerr << std::endl;
|
||||
std::cerr << collection;
|
||||
std::cerr << std::endl;
|
||||
|
||||
std::cerr << "p3Wiki::submitCollection() pushing to RsGenExchange";
|
||||
std::cerr << std::endl;
|
||||
|
||||
RsGenExchange::publishGroup(token, collectionItem);
|
||||
return true;
|
||||
}
|
||||
|
@ -188,3 +220,64 @@ bool p3Wiki::submitComment(uint32_t &token, RsWikiComment &comment)
|
|||
}
|
||||
|
||||
|
||||
|
||||
std::ostream &operator<<(std::ostream &out, const RsWikiCollection &group)
|
||||
{
|
||||
out << "RsWikiCollection [ ";
|
||||
out << " Name: " << group.mMeta.mGroupName;
|
||||
out << " Desc: " << group.mDescription;
|
||||
out << " Category: " << group.mCategory;
|
||||
out << " ]";
|
||||
return out;
|
||||
}
|
||||
|
||||
std::ostream &operator<<(std::ostream &out, const RsWikiSnapshot &shot)
|
||||
{
|
||||
out << "RsWikiSnapshot [ ";
|
||||
out << "Title: " << shot.mMeta.mMsgName;
|
||||
out << "]";
|
||||
return out;
|
||||
}
|
||||
|
||||
std::ostream &operator<<(std::ostream &out, const RsWikiComment &comment)
|
||||
{
|
||||
out << "RsWikiComment [ ";
|
||||
out << "Title: " << comment.mMeta.mMsgName;
|
||||
out << "]";
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
/***** FOR TESTING *****/
|
||||
|
||||
std::string p3Wiki::genRandomId()
|
||||
{
|
||||
std::string randomId;
|
||||
for(int i = 0; i < 20; i++)
|
||||
{
|
||||
randomId += (char) ('a' + (RSRandom::random_u32() % 26));
|
||||
}
|
||||
|
||||
return randomId;
|
||||
}
|
||||
|
||||
void p3Wiki::generateDummyData()
|
||||
{
|
||||
|
||||
#define GEN_COLLECTIONS 10
|
||||
|
||||
int i;
|
||||
for(i = 0; i < GEN_COLLECTIONS; i++)
|
||||
{
|
||||
RsWikiCollection wiki;
|
||||
wiki.mMeta.mGroupId = genRandomId();
|
||||
wiki.mMeta.mGroupFlags = 0;
|
||||
wiki.mMeta.mGroupName = genRandomId();
|
||||
|
||||
uint32_t dummyToken = 0;
|
||||
submitCollection(dummyToken, wiki);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -60,9 +60,11 @@ virtual bool submitCollection(uint32_t &token, RsWikiCollection &collection);
|
|||
virtual bool submitSnapshot(uint32_t &token, RsWikiSnapshot &snapshot);
|
||||
virtual bool submitComment(uint32_t &token, RsWikiComment &comment);
|
||||
|
||||
virtual void generateDummyData();
|
||||
|
||||
private:
|
||||
|
||||
//std::string genRandomId();
|
||||
std::string genRandomId();
|
||||
// RsMutex mWikiMtx;
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue