PartDisign/WorkflowManager: initial implementation
This commit is contained in:
committed by
Stefan Tröger
parent
eec03e20cc
commit
9e47fea5ab
@@ -233,6 +233,8 @@ SET(PartDesignGuiModule_SRCS
|
||||
Utils.h
|
||||
Workbench.cpp
|
||||
Workbench.h
|
||||
WorkflowManager.cpp
|
||||
WorkflowManager.h
|
||||
)
|
||||
SOURCE_GROUP("Module" FILES ${PartDesignGuiModule_SRCS})
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include <Mod/PartDesign/App/Feature.h>
|
||||
|
||||
#include "Utils.h"
|
||||
#include "WorkflowManager.h"
|
||||
|
||||
|
||||
|
||||
@@ -61,6 +62,9 @@ CmdPartDesignPart::CmdPartDesignPart()
|
||||
|
||||
void CmdPartDesignPart::activated(int iMsg)
|
||||
{
|
||||
if ( PartDesignGui::assureModernWorkflow( getDocument() ) )
|
||||
return;
|
||||
|
||||
openCommand("Add a part");
|
||||
std::string FeatName = getUniqueObjectName("Part");
|
||||
|
||||
@@ -76,7 +80,7 @@ void CmdPartDesignPart::activated(int iMsg)
|
||||
|
||||
bool CmdPartDesignPart::isActive(void)
|
||||
{
|
||||
return hasActiveDocument();
|
||||
return hasActiveDocument() && !PartDesignGui::isLegacyWorkflow ( getDocument () );
|
||||
}
|
||||
|
||||
//===========================================================================
|
||||
@@ -98,6 +102,8 @@ CmdPartDesignBody::CmdPartDesignBody()
|
||||
|
||||
void CmdPartDesignBody::activated(int iMsg)
|
||||
{
|
||||
if ( PartDesignGui::assureModernWorkflow( getDocument() ) )
|
||||
return;
|
||||
std::vector<App::DocumentObject*> features =
|
||||
getSelection().getObjectsOfType(Part::Feature::getClassTypeId());
|
||||
App::DocumentObject* baseFeature = nullptr;
|
||||
@@ -154,7 +160,40 @@ void CmdPartDesignBody::activated(int iMsg)
|
||||
|
||||
bool CmdPartDesignBody::isActive(void)
|
||||
{
|
||||
return hasActiveDocument();
|
||||
return hasActiveDocument() && !PartDesignGui::isLegacyWorkflow ( getDocument () );
|
||||
}
|
||||
|
||||
//===========================================================================
|
||||
// PartDesign_Migrate
|
||||
//===========================================================================
|
||||
|
||||
DEF_STD_CMD_A(CmdPartDesignMigrate);
|
||||
|
||||
CmdPartDesignMigrate::CmdPartDesignMigrate()
|
||||
: Command("PartDesign_Migrate")
|
||||
{
|
||||
sAppModule = "PartDesign";
|
||||
sGroup = QT_TR_NOOP("PartDesign");
|
||||
sMenuText = QT_TR_NOOP("Migrate");
|
||||
sToolTipText = QT_TR_NOOP("Migrate document to the new workflow");
|
||||
sWhatsThis = sToolTipText;
|
||||
sStatusTip = sToolTipText;
|
||||
}
|
||||
|
||||
void CmdPartDesignMigrate::activated(int iMsg)
|
||||
{
|
||||
App::Document *doc = getDocument();
|
||||
// TODO make a proper implementation
|
||||
QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Not implemented yet"),
|
||||
QObject::tr("The migration not implemented yet, just force-switching to the new workflow.\n"
|
||||
"Previous workflow was: %1").arg(int(
|
||||
PartDesignGui::WorkflowManager::instance()->determinWorkflow( doc ) )));
|
||||
PartDesignGui::WorkflowManager::instance()->forceWorkflow(doc, PartDesignGui::Workflow::Modern);
|
||||
}
|
||||
|
||||
bool CmdPartDesignMigrate::isActive(void)
|
||||
{
|
||||
return hasActiveDocument() && !PartDesignGui::isLegacyWorkflow ( getDocument () );
|
||||
}
|
||||
|
||||
//===========================================================================
|
||||
@@ -290,10 +329,7 @@ void CmdPartDesignDuplicateSelection::activated(int iMsg) {
|
||||
|
||||
bool CmdPartDesignDuplicateSelection::isActive(void)
|
||||
{
|
||||
if (getActiveGuiDocument())
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
return hasActiveDocument();
|
||||
}
|
||||
|
||||
//===========================================================================
|
||||
@@ -387,7 +423,7 @@ void CmdPartDesignMoveFeature::activated(int iMsg)
|
||||
} catch (Base::Exception &) {
|
||||
QMessageBox::warning( Gui::getMainWindow(), QObject::tr("Sketch plane cannot be migrated"),
|
||||
QObject::tr("Please edit '%1' and redefine it to use a Base or Datum plane as the sketch plane.").
|
||||
arg( QString::fromAscii( sketch->getNameInDocument() ) ) );
|
||||
arg( QString::fromAscii( sketch->Label.getValue () ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,6 +433,7 @@ void CmdPartDesignMoveFeature::activated(int iMsg)
|
||||
|
||||
bool CmdPartDesignMoveFeature::isActive(void)
|
||||
{
|
||||
return hasActiveDocument () && !PartDesignGui::isLegacyWorkflow ( getDocument () );
|
||||
return hasActiveDocument ();
|
||||
}
|
||||
|
||||
@@ -494,7 +531,7 @@ void CmdPartDesignMoveFeatureInTree::activated(int iMsg)
|
||||
|
||||
bool CmdPartDesignMoveFeatureInTree::isActive(void)
|
||||
{
|
||||
return hasActiveDocument ();
|
||||
return hasActiveDocument () && !PartDesignGui::isLegacyWorkflow ( getDocument () );
|
||||
}
|
||||
|
||||
|
||||
@@ -508,6 +545,7 @@ void CreatePartDesignBodyCommands(void)
|
||||
|
||||
rcCmdMgr.addCommand(new CmdPartDesignPart());
|
||||
rcCmdMgr.addCommand(new CmdPartDesignBody());
|
||||
rcCmdMgr.addCommand(new CmdPartDesignMigrate());
|
||||
rcCmdMgr.addCommand(new CmdPartDesignMoveTip());
|
||||
|
||||
rcCmdMgr.addCommand(new CmdPartDesignDuplicateSelection());
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
|
||||
#include "Workbench.h"
|
||||
|
||||
#include "WorkflowManager.h"
|
||||
|
||||
using namespace PartDesignGui;
|
||||
|
||||
#if 0 // needed for Qt's lupdate utility
|
||||
@@ -56,12 +58,11 @@ using namespace PartDesignGui;
|
||||
/// @namespace PartDesignGui @class Workbench
|
||||
TYPESYSTEM_SOURCE(PartDesignGui::Workbench, Gui::StdWorkbench)
|
||||
|
||||
Workbench::Workbench()
|
||||
{
|
||||
Workbench::Workbench() {
|
||||
}
|
||||
|
||||
Workbench::~Workbench()
|
||||
{
|
||||
Workbench::~Workbench() {
|
||||
WorkflowManager::destruct();
|
||||
}
|
||||
|
||||
// Commented out due to later to be moves and/or generall rewrighted from scratch (Fat-Zer 2015-08-08)
|
||||
@@ -370,6 +371,7 @@ void Workbench::activated()
|
||||
{
|
||||
Gui::Workbench::activated();
|
||||
|
||||
WorkflowManager::init();
|
||||
|
||||
std::vector<Gui::TaskView::TaskWatcher*> Watcher;
|
||||
|
||||
@@ -630,7 +632,9 @@ Gui::MenuItem* Workbench::setupMenuBar() const
|
||||
<< "PartDesign_Boolean"
|
||||
<< "Separator"
|
||||
//<< "PartDesign_Hole"
|
||||
<< "PartDesign_InvoluteGear";
|
||||
<< "PartDesign_InvoluteGear"
|
||||
<< "Separator"
|
||||
<< "PartDesign_Migrate";
|
||||
|
||||
// For 0.13 a couple of python packages like numpy, matplotlib and others
|
||||
// are not deployed with the installer on Windows. Thus, the WizardShaft is
|
||||
|
||||
218
src/Mod/PartDesign/Gui/WorkflowManager.cpp
Normal file
218
src/Mod/PartDesign/Gui/WorkflowManager.cpp
Normal file
@@ -0,0 +1,218 @@
|
||||
/***************************************************************************
|
||||
* Copyright (C) 2015 Alexander Golubev (Fat-Zer) <fatzer2@gmail.com> *
|
||||
* *
|
||||
* This file is part of the FreeCAD CAx development system. *
|
||||
* *
|
||||
* This library is free software; you can redistribute it and/or *
|
||||
* modify it under the terms of the GNU Library General Public *
|
||||
* License as published by the Free Software Foundation; either *
|
||||
* version 2 of the License, or (at your option) any later version. *
|
||||
* *
|
||||
* 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; see the file COPYING.LIB. If not, *
|
||||
* write to the Free Software Foundation, Inc., 59 Temple Place, *
|
||||
* Suite 330, Boston, MA 02111-1307, USA *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#include "PreCompiled.h"
|
||||
|
||||
#ifndef _PreComp_
|
||||
#include <vector>
|
||||
#include <list>
|
||||
#include <set>
|
||||
#include <boost/bind.hpp>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#endif
|
||||
|
||||
#include <Base/Exception.h>
|
||||
#include <App/Application.h>
|
||||
#include <App/Document.h>
|
||||
#include <Gui/MainWindow.h>
|
||||
#include <Gui/Command.h>
|
||||
#include <Gui/Application.h>
|
||||
#include <Mod/PartDesign/App/Body.h>
|
||||
#include <Mod/PartDesign/App/Feature.h>
|
||||
#include "WorkflowManager.h"
|
||||
|
||||
|
||||
using namespace PartDesignGui;
|
||||
|
||||
|
||||
WorkflowManager * WorkflowManager::_instance = nullptr;
|
||||
|
||||
|
||||
WorkflowManager::WorkflowManager() {
|
||||
// Fill the map with already opened documents
|
||||
for ( auto doc : App::GetApplication().getDocuments() ) {
|
||||
slotFinishRestoreDocument ( *doc );
|
||||
}
|
||||
|
||||
connectNewDocument = App::GetApplication().signalNewDocument.connect(
|
||||
boost::bind( &WorkflowManager::slotNewDocument, this, _1 ) );
|
||||
connectFinishRestoreDocument = App::GetApplication().signalFinishRestoreDocument.connect(
|
||||
boost::bind( &WorkflowManager::slotFinishRestoreDocument, this, _1 ) );
|
||||
connectDeleteDocument = App::GetApplication().signalDeleteDocument.connect(
|
||||
boost::bind( &WorkflowManager::slotDeleteDocument, this, _1 ) );
|
||||
}
|
||||
|
||||
WorkflowManager::~WorkflowManager() {
|
||||
// won't they will be disconnected on destruction?
|
||||
connectNewDocument.disconnect ();
|
||||
connectFinishRestoreDocument.disconnect ();
|
||||
}
|
||||
|
||||
|
||||
// Those destruction/construction is not really needed and could be done in the instance()
|
||||
// but to make things a bit more cleare better to keep them around.
|
||||
void WorkflowManager::init() {
|
||||
if (_instance) {
|
||||
throw Base::Exception( "Trying to init the workflow manager second time." );
|
||||
}
|
||||
_instance = new WorkflowManager();
|
||||
}
|
||||
|
||||
WorkflowManager *WorkflowManager::instance() {
|
||||
if (!_instance) {
|
||||
throw Base::Exception( "Trying to instance the WorkflowManager manager before init() was called." );
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
|
||||
void WorkflowManager::destruct() {
|
||||
if (_instance) {
|
||||
delete _instance;
|
||||
_instance = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void WorkflowManager::slotNewDocument( const App::Document &doc ) {
|
||||
// new document always uses new workflow
|
||||
dwMap[&doc] = Workflow::Modern;
|
||||
}
|
||||
|
||||
|
||||
void WorkflowManager::slotFinishRestoreDocument( const App::Document &doc ) {
|
||||
Workflow wf = guessWorkflow (&doc);
|
||||
// Mark document as undetermined if the guessed workflow is not new
|
||||
if( wf != Workflow::Modern ) {
|
||||
wf = Workflow::Undetermined;
|
||||
}
|
||||
dwMap[&doc] = wf;
|
||||
}
|
||||
|
||||
void WorkflowManager::slotDeleteDocument( const App::Document &doc ) {
|
||||
dwMap.erase(&doc);
|
||||
}
|
||||
|
||||
Workflow WorkflowManager::getWorkflowForDocument( App::Document *doc) {
|
||||
assert (doc);
|
||||
|
||||
auto it = dwMap.find(doc);
|
||||
|
||||
if ( it!=dwMap.end() ) {
|
||||
return it->second;
|
||||
} else {
|
||||
// We haven't yet checked the file workflow
|
||||
// May happen if e.g. file not compleatly loaded yet
|
||||
return Workflow::Undetermined;
|
||||
}
|
||||
}
|
||||
|
||||
Workflow WorkflowManager::determinWorkflow( App::Document *doc) {
|
||||
Workflow rv = getWorkflowForDocument (doc);
|
||||
|
||||
if (rv != Workflow::Undetermined) {
|
||||
// Return if workflow is known
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Guess the workflow again
|
||||
rv = guessWorkflow (doc);
|
||||
|
||||
if (rv != Workflow::Modern) {
|
||||
QMessageBox msgBox;
|
||||
|
||||
if ( rv == Workflow::Legacy ) { // legacy messages
|
||||
msgBox.setText( QObject::tr( "The document \"%1\" you are editing was design with old version of "
|
||||
"PartDesign workbench." ).arg( QString::fromStdString ( doc->getName()) ) );
|
||||
msgBox.setInformativeText (
|
||||
QObject::tr( "Do you want to migrate in order to use modern PartDesign features?" ) );
|
||||
} else { // The document is already in the middle of migration
|
||||
msgBox.setText( QObject::tr( "The document \"%1\" seems to be either in the middle of"
|
||||
" the migration process from legacy PartDesign or have a slightly broken structure."
|
||||
).arg( QString::fromStdString ( doc->getName()) ) );
|
||||
msgBox.setInformativeText (
|
||||
QObject::tr( "Do you want to make the migration automatically?" ) );
|
||||
}
|
||||
msgBox.setDetailedText( QObject::tr( "Note If you choose to migrate you won't be able to edit"
|
||||
" the file wtih old FreeCAD versions.\n"
|
||||
"If you refuse to migrate you won't be able to use new PartDesign features"
|
||||
" like Bodies and Parts. As a result you also won't be able to use your parts"
|
||||
" in the assembly workbench.\n"
|
||||
"Although you will be able to migrate any moment later with 'Part Design->Migrate'." ) );
|
||||
msgBox.setIcon( QMessageBox::Question );
|
||||
QPushButton * yesBtn = msgBox.addButton ( QMessageBox::Yes );
|
||||
QPushButton * manuallyBtn = msgBox.addButton (
|
||||
QObject::tr ( "Migrate manually" ), QMessageBox::YesRole );
|
||||
// If it is already a document in the middle of the migration the user shouldn't refuse to migrate
|
||||
if ( rv != Workflow::Undetermined ) {
|
||||
msgBox.addButton ( QMessageBox::No );
|
||||
}
|
||||
msgBox.setDefaultButton ( yesBtn );
|
||||
// TODO Add some description of manual migration mode (2015-08-09, Fat-Zer)
|
||||
|
||||
msgBox.exec();
|
||||
|
||||
if ( msgBox.clickedButton() == yesBtn ) {
|
||||
// TODO Assure that this will actually work (2015-08-02, Fat-Zer)
|
||||
Gui::Application::Instance->commandManager().runCommandByName("PartDesign_Migrate");
|
||||
rv = Workflow::Modern;
|
||||
} else if ( msgBox.clickedButton() == manuallyBtn ) {
|
||||
rv = Workflow::Modern;
|
||||
} else {
|
||||
rv = Workflow::Legacy;
|
||||
}
|
||||
}
|
||||
|
||||
// Actually set the result in our map
|
||||
dwMap[ doc ] = rv;
|
||||
return rv;
|
||||
}
|
||||
|
||||
void WorkflowManager::forceWorkflow( const App::Document *doc, Workflow wf) {
|
||||
dwMap[ doc ] = wf;
|
||||
}
|
||||
|
||||
Workflow WorkflowManager::guessWorkflow(const App::Document *doc) {
|
||||
// Retrive bodies of the document
|
||||
auto features = doc->getObjectsOfType( PartDesign::Feature::getClassTypeId() );
|
||||
|
||||
if( features.empty() ) {
|
||||
// a new file should be done in the new workflow
|
||||
return Workflow::Modern;
|
||||
} else {
|
||||
auto bodies = doc->getObjectsOfType( PartDesign::Body::getClassTypeId() );
|
||||
if (bodies.empty()) {
|
||||
// If there are no bodies workflow is legacy
|
||||
return Workflow::Legacy;
|
||||
} else {
|
||||
bool features_without_bodies = false;
|
||||
|
||||
for( auto feat: features ) {
|
||||
if( !PartDesign::Body::findBodyOf( feat ) ) {
|
||||
features_without_bodies = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// if there are features not belonging to any body itmeans that migration was incomplete, otherwice it's Modern
|
||||
return features_without_bodies ? Workflow::Undetermined : Workflow::Modern;
|
||||
}
|
||||
}
|
||||
}
|
||||
131
src/Mod/PartDesign/Gui/WorkflowManager.h
Normal file
131
src/Mod/PartDesign/Gui/WorkflowManager.h
Normal file
@@ -0,0 +1,131 @@
|
||||
/***************************************************************************
|
||||
* Copyright (C) 2015 Alexander Golubev (Fat-Zer) <fatzer2@gmail.com> *
|
||||
* *
|
||||
* This file is part of the FreeCAD CAx development system. *
|
||||
* *
|
||||
* This library is free software; you can redistribute it and/or *
|
||||
* modify it under the terms of the GNU Library General Public *
|
||||
* License as published by the Free Software Foundation; either *
|
||||
* version 2 of the License, or (at your option) any later version. *
|
||||
* *
|
||||
* 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; see the file COPYING.LIB. If not, *
|
||||
* write to the Free Software Foundation, Inc., 59 Temple Place, *
|
||||
* Suite 330, Boston, MA 02111-1307, USA *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef WORKFLOWMANAGER_H_PB7A5GCM
|
||||
#define WORKFLOWMANAGER_H_PB7A5GCM
|
||||
|
||||
#include <boost/signals.hpp>
|
||||
#include <map>
|
||||
|
||||
namespace App {
|
||||
class Document;
|
||||
}
|
||||
|
||||
namespace PartDesignGui {
|
||||
|
||||
/**
|
||||
* Defines allowded tool set provided by the workbench
|
||||
* Legacy mode provides a free PartDesign features but forbids bodies and parts
|
||||
*/
|
||||
enum class Workflow {
|
||||
Undetermined = 0, ///< No workflow was choosen yet
|
||||
Legacy = 1<<0, ///< Old-style workflow with free features and no bodies
|
||||
Modern = 1<<1, ///< New-style workflow with bodies, parts etc
|
||||
};
|
||||
|
||||
/**
|
||||
* This class controls the workflow of each file.
|
||||
* It has been introdused to support legacy files migrating to the new workflow.
|
||||
*/
|
||||
class PartDesignGuiExport WorkflowManager {
|
||||
public:
|
||||
virtual ~WorkflowManager ();
|
||||
|
||||
/**
|
||||
* Lookup the workflow of the document in the map.
|
||||
* If the document not in the map yet return Workflow::Undetermined.
|
||||
*/
|
||||
|
||||
Workflow getWorkflowForDocument(App::Document *doc);
|
||||
|
||||
/**
|
||||
* Asserts the workflow of the document to be determined and prompt user to migrate if it is not modern.
|
||||
*
|
||||
* If workflow was already choosen return it.
|
||||
* If the guesed workflow is Workflow::Legacy or Workflow::Mixed the user will be prompted to migrate.
|
||||
* If the user agrees the file will be migrated and the workflow will be set as modern.
|
||||
* If the user refuses to migrate use the old workflow.
|
||||
*/
|
||||
Workflow determinWorkflow(App::Document *doc);
|
||||
|
||||
/**
|
||||
* Force the desired workflow in document
|
||||
* Note: currently added for testing purpose; May be removed later
|
||||
*/
|
||||
void forceWorkflow (const App::Document *doc, Workflow wf);
|
||||
|
||||
/** @name Init, Destruct an Access methods */
|
||||
//@{
|
||||
/// Creates an instance of the manager, should be called before any instance()
|
||||
static void init ();
|
||||
/// Return an instance of the WorkflofManager.
|
||||
static WorkflowManager* instance();
|
||||
/// destroy the manager
|
||||
static void destruct ();
|
||||
//@}
|
||||
|
||||
private:
|
||||
/// The class is not intended to be constructed outside of itself
|
||||
WorkflowManager ();
|
||||
/// Get the signal on New document created
|
||||
void slotNewDocument (const App::Document& doc);
|
||||
/// Get the signal on document getting loaded
|
||||
void slotFinishRestoreDocument (const App::Document& doc);
|
||||
/// Get the signal on document close and remove it from our list
|
||||
void slotDeleteDocument (const App::Document& doc);
|
||||
|
||||
/// Guess the Workflow of the document out of it's content
|
||||
Workflow guessWorkflow(const App::Document *doc);
|
||||
|
||||
private:
|
||||
std::map<const App::Document*, Workflow> dwMap;
|
||||
|
||||
boost::signals::connection connectNewDocument;
|
||||
boost::signals::connection connectFinishRestoreDocument;
|
||||
boost::signals::connection connectDeleteDocument;
|
||||
|
||||
static WorkflowManager* _instance;
|
||||
};
|
||||
|
||||
/// Assures that workflow of the given document is determined and returns true if it is Workflow::Legacy
|
||||
inline bool assureLegacyWorkflow (App::Document *doc) {
|
||||
return WorkflowManager::instance()->determinWorkflow( doc ) == Workflow::Legacy ;
|
||||
}
|
||||
|
||||
/// Assures that workflow of the given document is determined and returns true if it is Workflow::Modern
|
||||
inline bool assureModernWorkflow (App::Document *doc) {
|
||||
return WorkflowManager::instance()->determinWorkflow( doc ) == Workflow::Modern ;
|
||||
}
|
||||
|
||||
/// Returns true if the workflow of the given document is Workflow::Legacy
|
||||
inline bool isLegacyWorkflow (App::Document *doc) {
|
||||
return WorkflowManager::instance()->getWorkflowForDocument( doc ) == Workflow::Legacy ;
|
||||
}
|
||||
|
||||
/// Returns true if the workflow of the given document is Workflow::Modern
|
||||
inline bool isModernWorkflow (App::Document *doc) {
|
||||
return WorkflowManager::instance()->getWorkflowForDocument( doc ) == Workflow::Modern ;
|
||||
}
|
||||
|
||||
} /* PartDesignGui */
|
||||
|
||||
#endif /* end of include guard: WORKFLOWMANAGER_H_PB7A5GCM */
|
||||
Reference in New Issue
Block a user