Skip to content
Snippets Groups Projects
Commit 4803bd4e authored by inso's avatar inso
Browse files

Network refactoring and removing QThread

- New behaviour for network traversal
- New requests patter -> we get the cache data instantly
while we are requesting the network in background
parents 5dd8919d ec419bbd
No related branches found
No related tags found
No related merge requests found
Showing
with 455 additions and 240 deletions
doc/uml/api.png

13.8 KiB

@startuml
package api {
package api.bma {
class BMADataAccess {
{static} _cache
{static} _request(req : Request, network)
{static} _post(req : Request, network)
{static} _broadcast(req : Request, network)
}
BMADataAccess ..> api.bma.API
}
package api.es {
class ESDataAccess {
}
ESDataAccess ..> api.es.API
}
}
@enduml
\ No newline at end of file
doc/uml/core-classes.png

55.5 KiB

@startuml
package core {
class App {
-- Signals --
current_account_changed(str : account_name)
data_changed()
-- Slots --
-- Properties --
current_account
accounts
-- Methods --
}
App --* Account : accounts
class Account {
-- Signals --
wallets_changed(int : nb_wallets)
community_added(int : index)
community_removed(int : index)
data_changed()
-- Slots --
-- Properties --
communities
wallets
-- Methods --
}
Account "1" --* "*" Wallet
Account "1" --* "*" Community
class Wallet {
-- Signals --
money_received(Transfer)
money_sent(Transfer)
name_changed(str : new_name
data_changed()
-- Slots --
-- Properties --
transfers
-- Methods --
}
Wallet "1" --* "*" Transfer
class Transfer {
-- Signals --
state_changed(int : new_state)
-- Slots --
-- Properties --
-- Methods --
}
class Community {
-- Signals --
members_changed()
data_changed()
-- Slots --
-- Properties --
network
-- Methods --
}
App --> Identity
class Identity {
{static} _identities
{static} load(data : dict)
{static} lookup(search : str)
}
}
package net {
class Network {
-- Signals --
node_found(int : index)
node_removed(int : index)
block_found(int : block_number)
-- Slots --
-- Properties --
nodes
root_nodes
-- Methods --
}
Community "1" --* "1" Network
Network "1" --* "*" Node
class Node {
-- Signals --
changed()
-- Slots --
-- Properties --
endpoints
pubkey
uid
block
state
-- Methods --
}
}
@enduml
\ No newline at end of file
doc/uml/cutecoin.png

261 KiB

@startuml
!include core-classes.pu
!include gui-classes.pu
!include models-classes.pu
!include api.pu
MainWindow "1" --> "1" App
CertificationDialog --> Community
TransferDialog --> Community
CurrencyTab "1" --> "1" Community
CommunityTab -right-> IdentitiesFilterProxyModel
NetworkTab -right-> NetworkFilterProxyModel
WalletTab -right-> WalletsFilterProxyModel
WalletsFilterProxyModel -up-> Wallet
NetworkFilterProxyModel -up-> Network
TxHistoryFilterProxyModel -up-> Transfer
ConfigureAccountDialog --> CommunitiesListModel
ConfigureCommunityDialog --> RootNodesTableModel
ConfigureAccountDialog --> Account
ConfigureCommunityDialog --> Community
Account ..> BMADataAccess
Community ..> BMADataAccess
Wallet ..> BMADataAccess
Transfer ..> BMADataAccess
Identity ..> BMADataAccess
BMADataAccess .left.> Network
@enduml
\ No newline at end of file
doc/uml/gui-classes.png

38.8 KiB

@startuml
package gui {
class MainWindow {
}
MainWindow "1" --* "*" CurrencyTab
class CurrencyTab {
}
CurrencyTab "1" --* "1" CommunityTab
CurrencyTab "1" --* "1" WalletTab
CurrencyTab "1" --* "1" InformationsTab
CurrencyTab "1" --* "1" TransactionsTab
class CommunityTab {
}
CommunityTab "1" --* "1" IdentitiesTab
CommunityTab "1" --* "1" WotTab
class WalletTab {
}
class InformationsTab {
}
class TransactionsTab {
}
class NetworkTab {
}
CurrencyTab "1" --* "1" NetworkTab
class IdentitiesTab {
}
class WotTab {
}
package dialogs {
class CertificationDialog
class TransferDialog
class ContactDialog
class ConfigureAccountDialog
class ConfigureCommunityDialog
}
MainWindow --> CertificationDialog
MainWindow --> TransferDialog
MainWindow --> ContactDialog
MainWindow --> ConfigureAccountDialog
ConfigureAccountDialog --> ConfigureCommunityDialog
class Wot
WotTab --> Wot
}
@enduml
\ No newline at end of file
doc/uml/models-classes.png

14.8 KiB

@startuml
package models {
class WalletsFilterProxyModel {
}
WalletsFilterProxyModel --> WalletsTableModel : source
class WalletsTableModel {
}
class IdentitiesFilterProxyModel {
}
IdentitiesFilterProxyModel --> IdentitiesTableModel : source
class IdentitiesTableModel {
}
class NetworkFilterProxyModel {
}
NetworkFilterProxyModel --> NetworkTableModel : source
class NetworkTableModel {
}
class TxHistoryFilterProxyModel {
}
TxHistoryFilterProxyModel --> TxHistoryTableModel : source
class TxHistoryTableModel {
}
class CommunitiesListModel {
}
class RootNodesTableModel {
}
}
@enduml
\ No newline at end of file
doc/uml/network.png

45.7 KiB

@startuml
Network -->o Node : Connect to node_received()
Network -> Node : Starts network discovery
activate Node
Node -> QNetworkManager : HTTP GET peering/peers?leaves=true
create QNetworkReply
QNetworkManager -> QNetworkReply : Instantiate
Node <- QNetworkManager : QNetworkReply
Node -->o QNetworkReply : Connect to finished()
Network <- Node
deactivate Node
... Request is processed ...
Node <-- QNetworkReply : finished()
destroy QNetworkReply
alt "root" hash changed
loop "for all leaves changed"
activate Node
Node -> QNetworkManager : HTTP GET peering/peers/leaf=leaf_hash
create QNetworkReply
QNetworkManager -> QNetworkReply : Instantiate
Node <- QNetworkManager : QNetworkReply
Node -->o QNetworkReply : Connect to finished()
end
end
... Requests is processed ...
Node <-- QNetworkReply : finished()
destroy QNetworkReply
Network <-- Node : node_received()
ref over Network
New node is instanciated
if pubkey not known yet.
It starts it's own
network discovery
end ref
@enduml
\ No newline at end of file
doc/uml/requests.png

57.4 KiB

@startuml
QModel -->o "Core Component" : Connect to data_changed()
QModel -> "Core Component" : Data access
activate "Core Component"
"Core Component" -> Community : Request data
Community -> Cache : Request cache
ref over Cache
Data is obsolete
(new block mined
since last caching)
end ref
Cache -> QNetworkManager : HTTP GET
create QNetworkReply
QNetworkManager -> QNetworkReply : Instantiate
Cache <- QNetworkManager : QNetworkReply
create ReceiverSlot
Cache -> ReceiverSlot : Instantiate Slot
QNetworkReply o<-- ReceiverSlot : Connect to finished()
Community <- Cache : Cached data
"Core Component" <- Community : Cached data
"Core Component" -> "Core Component" : Compute data
QModel <- "Core Component" : Data
deactivate "Core Component"
...Network request is processed...
ReceiverSlot <-- QNetworkReply : finished()
activate ReceiverSlot
ReceiverSlot -> Cache : Update cache data
ReceiverSlot -> "Core Component" : emit data_changed()
deactivate ReceiverSlot
destroy ReceiverSlot
destroy QNetworkReply
|||
QModel <-- "Core Component" : data_changed()
QModel -> "Core Component" : Data access
activate "Core Component"
ref over "Core Component", Community
Community is requested again,
and last cached data are returned
No new block mined, so no HTTP GET
initialized between cache
and QNetworkManager
end ref
QModel <- "Core Component" : Data
deactivate "Core Component"
@enduml
\ No newline at end of file
doc/uml/tx_lifecycle.png

22 KiB | W: | H:

doc/uml/tx_lifecycle.png

21.4 KiB | W: | H:

doc/uml/tx_lifecycle.png
doc/uml/tx_lifecycle.png
doc/uml/tx_lifecycle.png
doc/uml/tx_lifecycle.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -293,4 +293,4 @@ PreviousIssuer: {1}\n".format(self.prev_hash, self.prev_issuer)
for transaction in self.transactions:
doc += "{0}\n".format(transaction.compact())
return doc
return doc
\ No newline at end of file
......@@ -41,14 +41,21 @@ def relative(units, community):
:param cutecoin.core.community.Community community: Community instance
:return: float
"""
# fixme: the value "community.nb_members" is not up to date, luckyly the good value is in "community.get_ud_block()['membersCount']"
# calculate ud(t+1)
ud = math.ceil(
max(community.dividend,
community.parameters['c'] * community.monetary_mass / community.get_ud_block()['membersCount'])
)
relative_value = units / float(ud)
return relative_value
ud_block = community.get_ud_block()
if ud_block:
ud = math.ceil(
max(community.dividend(),
float(0) if ud_block['membersCount'] == 0 else
community.parameters['c'] * community.monetary_mass / ud_block['membersCount']))
if ud == 0:
return float(0)
else:
relative_value = units / float(ud)
return relative_value
else:
return float(0)
def quantitative_zerosum(units, community):
......@@ -111,6 +118,8 @@ class Account(QObject):
)
loading_progressed = pyqtSignal(int, int)
inner_data_changed = pyqtSignal()
wallets_changed = pyqtSignal()
def __init__(self, salt, pubkey, name, communities, wallets, contacts):
'''
......@@ -153,9 +162,10 @@ class Account(QObject):
return account
@classmethod
def load(cls, json_data):
def load(cls, network_manager, json_data):
'''
Factory method to create an Account object from its json view.
:rtype : cutecoin.core.account.Account
:param dict json_data: The account view as a json dict
:return: A new account object created from the json datas
'''
......@@ -174,7 +184,7 @@ class Account(QObject):
communities = []
for data in json_data['communities']:
community = Community.load(data)
community = Community.load(network_manager, data)
communities.append(community)
account = cls(salt, pubkey, name, communities, wallets,
......@@ -282,6 +292,7 @@ class Account(QObject):
self.wallets.append(wallet)
else:
self.wallets = self.wallets[:size]
self.wallets_changed.emit()
def certify(self, password, community, pubkey):
"""
......
......@@ -19,7 +19,6 @@ from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkReques
from . import config
from .account import Account
from . import person
from .watching.monitor import Monitor
from .. import __version__
from ..tools.exceptions import NameAlreadyExists, BadAccountFile
......@@ -44,13 +43,11 @@ class Application(QObject):
super().__init__()
self.accounts = {}
self.current_account = None
self.monitor = None
self.available_version = (True,
__version__,
"")
config.parse_arguments(argv)
self._network_manager = QNetworkAccessManager()
self._network_manager.finished.connect(self.read_available_version)
self.preferences = {'account': "",
'lang': 'en_GB',
'ref': 0
......@@ -126,13 +123,9 @@ class Application(QObject):
self.loading_progressed.emit(value, maximum)
if self.current_account is not None:
if self.monitor:
self.monitor.stop_watching()
self.save_cache(self.current_account)
account.loading_progressed.connect(progressing)
account.refresh_cache()
self.monitor = Monitor(account)
self.monitor.prepare_watching()
self.current_account = account
def load(self):
......@@ -178,7 +171,7 @@ class Application(QObject):
account_name, 'properties')
with open(account_path, 'r') as json_data:
data = json.load(json_data)
account = Account.load(data)
account = Account.load(self._network_manager, data)
self.load_cache(account)
self.accounts[account_name] = account
......@@ -189,9 +182,9 @@ class Application(QObject):
:param account: The account object to load the cache
'''
for community in account.communities:
community_path = os.path.join(config.parameters['home'],
bma_path = os.path.join(config.parameters['home'],
account.name, '__cache__',
community.currency)
community.currency + '_bma')
network_path = os.path.join(config.parameters['home'],
account.name, '__cache__',
......@@ -202,17 +195,17 @@ class Application(QObject):
data = json.load(json_data)
if 'version' in data and data['version'] == __version__:
logging.debug("Merging network : {0}".format(data))
community.load_merge_network(data['network'])
community.network.merge_with_json(data['network'])
else:
os.remove(network_path)
if os.path.exists(community_path):
with open(community_path, 'r') as json_data:
if os.path.exists(bma_path):
with open(bma_path, 'r') as json_data:
data = json.load(json_data)
if 'version' in data and data['version'] == __version__:
community.load_cache(data)
community.bma_access.load_from_json(data['cache'])
else:
os.remove(community_path)
os.remove(bma_path)
for wallet in account.wallets:
wallet_path = os.path.join(config.parameters['home'],
......@@ -319,9 +312,9 @@ class Application(QObject):
self.save_wallet(account, wallet)
for community in account.communities:
community_path = os.path.join(config.parameters['home'],
bma_path = os.path.join(config.parameters['home'],
account.name, '__cache__',
community.currency)
community.currency + '_bma')
network_path = os.path.join(config.parameters['home'],
account.name, '__cache__',
......@@ -329,12 +322,12 @@ class Application(QObject):
with open(network_path, 'w') as outfile:
data = dict()
data['network'] = community.jsonify_network()
data['network'] = community.network.jsonify()
data['version'] = __version__
json.dump(data, outfile, indent=4, sort_keys=True)
with open(community_path, 'w') as outfile:
data = community.jsonify_cache()
with open(bma_path, 'w') as outfile:
data['cache'] = community.bma_access.jsonify()
data['version'] = __version__
json.dump(data, outfile, indent=4, sort_keys=True)
......@@ -402,11 +395,13 @@ class Application(QObject):
def get_last_version(self):
url = QUrl("https://api.github.com/repos/ucoin-io/cutecoin/releases")
request = QNetworkRequest(url)
self._network_manager.get(request)
reply = self._network_manager.get(request)
reply.finished.connect(self.read_available_version)
@pyqtSlot(QNetworkReply)
def read_available_version(self, reply):
def read_available_version(self):
latest = None
reply = self.sender()
releases = reply.readAll().data().decode('utf-8')
logging.debug(releases)
if reply.error() == QNetworkReply.NoError:
......
......@@ -4,200 +4,104 @@ Created on 1 févr. 2014
@author: inso
'''
from PyQt5.QtCore import QObject, pyqtSignal
from ucoinpy.api import bma
from ucoinpy.documents.block import Block
from ..tools.exceptions import NoPeerAvailable
from .net.node import Node
from .net.network import Network
import logging
import inspect
import hashlib
import re
import time
from requests.exceptions import RequestException
class Cache():
_saved_requests = [str(bma.blockchain.Block), str(bma.blockchain.Parameters)]
def __init__(self, community):
'''
Init an empty cache
'''
self.latest_block = 0
self.community = community
self.data = {}
def load_from_json(self, data):
'''
Put data in the cache from json datas.
:param dict data: The cache in json format
'''
self.data = {}
for entry in data['cache']:
key = entry['key']
cache_key = (key[0], key[1], key[2], key[3], key[4])
self.data[cache_key] = entry['value']
self.latest_block = data['latest_block']
def jsonify(self):
'''
Get the cache in json format
:return: The cache as a dict in json format
'''
data = {k: self.data[k] for k in self.data.keys()}
entries = []
for d in data:
entries.append({'key': d,
'value': data[d]})
return {'latest_block': self.latest_block,
'cache': entries}
def refresh(self):
'''
Refreshing the cache just clears every last requests which
cannot be saved because they can change from one block to another.
'''
logging.debug("Refresh : {0}/{1}".format(self.latest_block,
self.community.network.latest_block))
if self.latest_block < self.community.network.latest_block:
self.latest_block = self.community.network.latest_block
self.data = {k: self.data[k] for k in self.data.keys()
if k[0] in Cache._saved_requests}
def request(self, request, req_args={}, get_args={}):
'''
Send a cached request to a community.
If the request was already sent in current block, return last value get.
from PyQt5.QtCore import QObject, pyqtSignal
from requests.exceptions import RequestException
:param request: The request bma class
:param req_args: The arguments passed to the request constructor
:param get_args: The arguments passed to the requests __get__ method
'''
cache_key = (str(request),
str(tuple(frozenset(sorted(req_args.keys())))),
str(tuple(frozenset(sorted(req_args.values())))),
str(tuple(frozenset(sorted(get_args.keys())))),
str(tuple(frozenset(sorted(get_args.values())))))
if cache_key not in self.data.keys():
result = self.community.request(request, req_args, get_args,
cached=False)
# For block 0, we should have a different behaviour
# Community members and certifications
# Should be requested without caching
self.data[cache_key] = result
return self.data[cache_key]
else:
return self.data[cache_key]
from ucoinpy.documents.block import Block
from ..tools.exceptions import NoPeerAvailable
from .net.network import Network
from .net.api import bma as qtbma
from .net.api.bma.access import BmaAccess
class Community(QObject):
'''
"""
A community is a group of nodes using the same currency.
.. warning:: The currency name is supposed to be unique in cutecoin
but nothing exists in ucoin to assert that a currency name is unique.
'''
"""
inner_data_changed = pyqtSignal(int)
def __init__(self, currency, network):
'''
def __init__(self, currency, network, bma_access):
"""
Initialize community attributes with a currency and a network.
:param str currency: The currency name of the community.
:param network: The network of the community
:param cutecoin.core.net.network.Network network: The network of the community
:param cutecoin.core.net.api.bma.access.BmaAccess bma_access: The BMA Access object
.. warning:: The community object should be created using its factory
class methods.
'''
"""
super().__init__()
self.currency = currency
self._network = network
self._cache = Cache(self)
self._cache.refresh()
self._bma_access = bma_access
@classmethod
def create(cls, node):
'''
"""
Create a community from its first node.
:param node: The first Node of the community
'''
"""
network = Network.create(node)
community = cls(node.currency, network)
bma_access = BmaAccess.create(network)
community = cls(node.currency, network, bma_access)
logging.debug("Creating community")
return community
@classmethod
def load(cls, json_data):
'''
def load(cls, network_manager, json_data):
"""
Load a community from json
:param dict json_data: The community as a dict in json format
'''
"""
currency = json_data['currency']
network = Network.from_json(currency, json_data['peers'])
community = cls(currency, network)
network = Network.from_json(network_manager, currency, json_data['peers'])
bma_access = BmaAccess.create(network)
community = cls(currency, network, bma_access)
return community
def load_merge_network(self, json_data):
'''
Load the last state of the network.
This network is merged with the network currently
known network.
:param dict json_data:
'''
self._network.merge_with_json(json_data)
def load_cache(self, json_data):
'''
def load_cache(self, bma_access_cache, network_cache):
"""
Load the community cache.
:param dict json_data: The community as a dict in json format.
'''
self._cache.load_from_json(json_data)
def jsonify_cache(self):
'''
Get the cache jsonified.
:return: The cache as a dict in json format.
'''
return self._cache.jsonify()
def jsonify_network(self):
'''
Get the network jsonified.
'''
return self._network.jsonify()
:param dict bma_access_cache: The BmaAccess cache in json
:param dict network_cache: The network cache in json
"""
self._bma_access.load_from_json(bma_access_cache)
self._network.merge_with_json(network_cache)
@property
def name(self):
'''
"""
The community name is its currency name.
:return: The community name
'''
"""
return self.currency
def __eq__(self, other):
'''
"""
:return: True if this community has the same currency name as the other one.
'''
"""
return (other.currency == self.currency)
@property
def short_currency(self):
'''
"""
Format the currency name to a short one
:return: The currency name in a shot format.
'''
"""
words = re.split('[_\W]+', self.currency)
shortened = ""
if len(words) > 1:
......@@ -210,22 +114,22 @@ class Community(QObject):
@property
def currency_symbol(self):
'''
"""
Format the currency name to a symbol one.
:return: The currency name as a utf-8 circled symbol.
'''
"""
letter = self.currency[0]
u = ord('\u24B6') + ord(letter) - ord('A')
return chr(u)
@property
#@property
def dividend(self):
'''
"""
Get the last generated community universal dividend.
:return: The last UD or 1 if no UD was generated.
'''
"""
block = self.get_ud_block()
if block:
return block['dividend']
......@@ -233,32 +137,32 @@ class Community(QObject):
return 1
def get_ud_block(self, x=0):
'''
"""
Get a block with universal dividend
:param int x: Get the 'x' older block with UD in it
:return: The last block with universal dividend.
'''
blocks = self.request(bma.blockchain.UD)['result']['blocks']
"""
blocks = self.bma_access.get(self, qtbma.blockchain.UD)['result']['blocks']
if len(blocks) > 0:
block_number = blocks[len(blocks)-(1+x)]
block = self.request(bma.blockchain.Block,
block = self.bma_access.get(self, qtbma.blockchain.Block,
req_args={'number': block_number})
return block
else:
return False
return None
@property
def monetary_mass(self):
'''
"""
Get the community monetary mass
:return: The monetary mass value
'''
"""
try:
# Get cached block by block number
block_number = self.network.latest_block
block = self.request(bma.blockchain.Block,
block = self.bma_access.get(self, qtbma.blockchain.Block,
req_args={'number': block_number})
return block['monetaryMass']
except ValueError as e:
......@@ -269,15 +173,15 @@ class Community(QObject):
@property
def nb_members(self):
'''
"""
Get the community members number
:return: The community members number
'''
"""
try:
# Get cached block by block number
block_number = self.network.latest_block
block = self.request(bma.blockchain.Block,
block = self.bma_access.get(qtbma.blockchain.Block,
req_args={'number': block_number})
return block['membersCount']
except ValueError as e:
......@@ -288,29 +192,30 @@ class Community(QObject):
@property
def network(self):
'''
"""
Get the community network instance.
:return: The community network instance.
'''
:rtype: cutecoin.core.net.network.Network
"""
return self._network
def network_quality(self):
'''
Get a ratio of the synced nodes vs the rest
'''
synced = len(self._network.synced_nodes)
#online = len(self._network.online_nodes)
total = len(self._network.nodes)
ratio_synced = synced / total
return ratio_synced
@property
def bma_access(self):
"""
Get the community bma_access instance
:return: The community bma_access instace
:rtype: cutecoin.core.net.api.bma.access.BmaAccess
"""
return self._bma_access
@property
def parameters(self):
'''
"""
Return community parameters in bma format
'''
return self.request(bma.blockchain.Parameters)
"""
return self.bma_access.get(self, qtbma.blockchain.Parameters)
def certification_expired(self, certtime):
'''
......@@ -341,14 +246,12 @@ class Community(QObject):
:param int number: The block number. If none, returns current block.
'''
if number is None:
data = self.request(bma.blockchain.Current)
data = self.bma_access.get(self, qtbma.blockchain.Current)
else:
logging.debug("Requesting block {0}".format(number))
data = self.request(bma.blockchain.Block,
data = self.bma_access.get(self, qtbma.blockchain.Block,
req_args={'number': number})
return Block.from_signed_raw("{0}{1}\n".format(data['raw'],
data['signature']))
return data
def current_blockid(self):
'''
......@@ -357,7 +260,7 @@ class Community(QObject):
:return: The current block ID as [NUMBER-HASH] format.
'''
try:
block = self.request(bma.blockchain.Current, cached=False)
block = self.bma_access.get(self, qtbma.blockchain.Current, cached=False)
signed_raw = "{0}{1}\n".format(block['raw'], block['signature'])
block_hash = hashlib.sha1(signed_raw.encode("ascii")).hexdigest().upper()
block_number = block['number']
......@@ -375,7 +278,7 @@ class Community(QObject):
:return: All members pubkeys.
'''
memberships = self.request(bma.wot.Members)
memberships = self.bma_access.get(self, qtbma.wot.Members)
return [m['pubkey'] for m in memberships["results"]]
def refresh_cache(self):
......@@ -384,42 +287,6 @@ class Community(QObject):
'''
self._cache.refresh()
def request(self, request, req_args={}, get_args={}, cached=True):
'''
Start a request to the community.
:param request: A ucoinpy bma request class
:param req_args: Arguments to pass to the request constructor
:param get_args: Arguments to pass to the request __get__ method
:return: The returned data
'''
if cached:
return self._cache.request(request, req_args, get_args)
else:
nodes = self._network.synced_nodes
for node in nodes:
try:
req = request(node.endpoint.conn_handler(), **req_args)
data = req.get(**get_args)
if inspect.isgenerator(data):
generated = []
for d in data:
generated.append(d)
return generated
else:
return data
except ValueError as e:
if '502' in str(e):
continue
else:
raise
except RequestException as e:
logging.debug("Error : {1} : {0}".format(str(e),
str(request)))
continue
raise NoPeerAvailable(self.currency, len(nodes))
def post(self, request, req_args={}, post_args={}):
'''
Post data to a community.
......
__author__ = 'inso'
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment