Merge pull request #38 from 67P/refactor/proposal
Refactor proposals
This commit was merged in pull request #38.
This commit is contained in:
@@ -1,67 +1,60 @@
|
|||||||
import Ember from 'ember';
|
import Component from 'ember-component';
|
||||||
|
import computed, { and } from 'ember-computed';
|
||||||
const {
|
import injectService from 'ember-service/inject';
|
||||||
Component,
|
import isPresent from 'kredits-web/utils/cps/is-present';
|
||||||
isPresent,
|
|
||||||
inject: {
|
|
||||||
service
|
|
||||||
},
|
|
||||||
computed
|
|
||||||
} = Ember;
|
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
|
kredits: injectService(),
|
||||||
|
|
||||||
kredits: service(),
|
// Default attributes used by reset
|
||||||
|
attributes: {
|
||||||
|
recipientId: null,
|
||||||
|
kind: 'community',
|
||||||
|
amount: null,
|
||||||
|
description: null,
|
||||||
|
url: null,
|
||||||
|
},
|
||||||
|
|
||||||
proposal: null,
|
didInsertElement() {
|
||||||
contributors: null,
|
this._super(...arguments);
|
||||||
inProgress: false,
|
this.reset();
|
||||||
|
},
|
||||||
|
|
||||||
isValidRecipient: computed('proposal.recipientAddress', function() {
|
contributors: [],
|
||||||
// TODO: add proper address validation
|
|
||||||
return this.get('proposal.recipientAddress') !== '';
|
isValidRecipient: isPresent('recipientId'),
|
||||||
|
isValidAmount: computed('amount', function() {
|
||||||
|
return parseInt(this.get('amount'), 10) > 0;
|
||||||
}),
|
}),
|
||||||
|
isValidDescription: isPresent('description'),
|
||||||
isValidAmount: computed('proposal.amount', function() {
|
isValidUrl: isPresent('url'),
|
||||||
return parseInt(this.get('proposal.amount'), 10) > 0;
|
isValid: and('isValidRecipient',
|
||||||
}),
|
|
||||||
|
|
||||||
isValidUrl: computed('proposal.url', function() {
|
|
||||||
return isPresent(this.get('proposal.url'));
|
|
||||||
}),
|
|
||||||
|
|
||||||
isValidDescription: computed('proposal.description', function() {
|
|
||||||
return isPresent(this.get('proposal.description'));
|
|
||||||
}),
|
|
||||||
|
|
||||||
isValid: computed.and('isValidRecipient',
|
|
||||||
'isValidAmount',
|
'isValidAmount',
|
||||||
'isValidDescription'),
|
'isValidDescription'),
|
||||||
|
|
||||||
|
reset: function() {
|
||||||
|
this.setProperties(this.get('attributes'));
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
save() {
|
submit() {
|
||||||
if (! this.get('isValid')) {
|
if (!this.get('isValid')) {
|
||||||
alert('Invalid data. Please review and try again.');
|
alert('Invalid data. Please review and try again.');
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
this.set('inProgress', true);
|
|
||||||
let proposal = this.get('proposal');
|
|
||||||
|
|
||||||
// Set the recipient's IPFS profile hash so it can be used in the
|
let attributes = Object.keys(this.get('attributes'));
|
||||||
// contribution object (which is to be stored in IPFS as well)
|
let proposal = this.getProperties(attributes);
|
||||||
let contributor = this.get('contributors').findBy('address', proposal.get('recipientAddress'));
|
let saved = this.save(proposal);
|
||||||
proposal.set('recipientProfile', contributor.get('ipfsHash'));
|
|
||||||
|
|
||||||
this.get('kredits').addProposal(proposal)
|
// The promise handles inProgress
|
||||||
.then(() => {
|
this.set('inProgress', saved);
|
||||||
this.attrs.onSave();
|
|
||||||
}).catch((error) => {
|
saved.then(() => {
|
||||||
Ember.Logger.error('[add-proposal] error creating the proposal', error);
|
this.reset();
|
||||||
alert('Something went wrong.');
|
window.scroll(0,0);
|
||||||
}).finally(() => {
|
window.alert('Contributor added.');
|
||||||
this.set('inProgress', false);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,41 +1,43 @@
|
|||||||
<form {{action "save" on="submit"}}>
|
<form {{action "submit" on="submit"}}>
|
||||||
<p>
|
<p>
|
||||||
<select required onchange={{action (mut proposal.recipientAddress) value="target.value"}}>
|
<select required onchange={{action (mut recipientId) value="target.value"}}>
|
||||||
<option value="" selected disabled hidden>Contributor</option>
|
<option value="" selected disabled hidden>Contributor</option>
|
||||||
{{#each contributors as |contributor|}}
|
{{#each contributors as |contributor|}}
|
||||||
<option value={{contributor.address}} selected={{eq proposal.recipientAddress contributor.address}}>{{contributor.github_username}}</option>
|
<option value={{contributor.id}} selected={{eq recipientId contributor.id}}>{{contributor.github_username}}</option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<select required onchange={{action (mut proposal.kind) value="target.value"}}>
|
<select required onchange={{action (mut kind) value="target.value"}}>
|
||||||
<option value="community" selected={{eq proposal.kind "community"}}>Community</option>
|
<option value="community" selected={{eq kind "community"}}>Community</option>
|
||||||
<option value="design" selected={{eq proposal.kind "design"}}>Design</option>
|
<option value="design" selected={{eq kind "design"}}>Design</option>
|
||||||
<option value="dev" selected={{eq proposal.kind "dev"}}>Development</option>
|
<option value="dev" selected={{eq kind "dev"}}>Development</option>
|
||||||
<option value="docs" selected={{eq proposal.kind "docs"}}>Documentation</option>
|
<option value="docs" selected={{eq kind "docs"}}>Documentation</option>
|
||||||
<option value="ops" selected={{eq proposal.kind "ops"}}>IT Operations</option>
|
<option value="ops" selected={{eq kind "ops"}}>IT Operations</option>
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{input type="text"
|
{{input type="text"
|
||||||
placeholder="100"
|
placeholder="100"
|
||||||
value=proposal.amount
|
value=amount
|
||||||
class=(if isValidAmount 'valid' '')}}
|
class=(if isValidAmount 'valid' '')}}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{input type="text"
|
{{input type="text"
|
||||||
placeholder="Description"
|
placeholder="Description"
|
||||||
value=proposal.description
|
value=description
|
||||||
class=(if isValidDescription 'valid' '')}}
|
class=(if isValidDescription 'valid' '')}}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{input type="text"
|
{{input type="text"
|
||||||
placeholder="URL (optional)"
|
placeholder="URL (optional)"
|
||||||
value=proposal.url
|
value=url
|
||||||
class=(if isValidUrl 'valid' '')}}
|
class=(if isValidUrl 'valid' '')}}
|
||||||
</p>
|
</p>
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
{{input type="submit" value=(if inProgress 'Processing' 'Save') disabled=inProgress}}
|
{{input type="submit"
|
||||||
|
disabled=(is-pending inProgress)
|
||||||
|
value=(if (is-pending inProgress) 'Processing' 'Save')}}
|
||||||
{{#link-to 'index'}}Back{{/link-to}}
|
{{#link-to 'index'}}Back{{/link-to}}
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
import Ember from 'ember';
|
import Controller from 'ember-controller';
|
||||||
import QueryParams from 'ember-parachute';
|
import { filterBy } from 'ember-computed';
|
||||||
|
import injectService from 'ember-service/inject';
|
||||||
|
|
||||||
export const queryParams = new QueryParams({
|
export default Controller.extend({
|
||||||
recipient: {
|
kredits: injectService(),
|
||||||
defaultValue: ''
|
|
||||||
},
|
|
||||||
amount: {
|
|
||||||
defaultValue: ''
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
defaultValue: ''
|
|
||||||
},
|
|
||||||
ipfsHash: {
|
|
||||||
defaultValue: ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Ember.Controller.extend(queryParams.Mixin, {
|
contributors: [],
|
||||||
|
minedContributors: filterBy('contributors', 'id'),
|
||||||
contributors: null,
|
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
onSave() {
|
save(proposal) {
|
||||||
this.transitionToRoute('index');
|
// contributorIpfsHash is needed for the proposal ipfs data. I'm not happy to do this here but I think to load all the contributors in addProposal again is a bit too much. I hope we can refactor it later.
|
||||||
}
|
let contributor = this.get('contributors').findBy('id', proposal.recipientId);
|
||||||
}
|
proposal.contributorIpfsHash = contributor.get('ipfsHash');
|
||||||
|
|
||||||
|
return this.get('kredits').addProposal(proposal)
|
||||||
|
.then((proposal) => {
|
||||||
|
this.transitionToRoute('index');
|
||||||
|
return proposal;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import ContributionSerializer from './serializers/contribution';
|
||||||
import ContributorSerializer from './serializers/contributor';
|
import ContributorSerializer from './serializers/contributor';
|
||||||
import { fromBytes32, toBytes32 } from './utils/multihash';
|
import { fromBytes32, toBytes32 } from './utils/multihash';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
ContributionSerializer,
|
||||||
ContributorSerializer,
|
ContributorSerializer,
|
||||||
fromBytes32,
|
fromBytes32,
|
||||||
toBytes32
|
toBytes32
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Handle serialization for JSON-LD object of the contribution, according to
|
||||||
|
* https://github.com/67P/kosmos-schemas/blob/master/schemas/contribution.json
|
||||||
|
*
|
||||||
|
* @class
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export default class Contributor {
|
||||||
|
/**
|
||||||
|
* Deserialize JSON to object
|
||||||
|
*
|
||||||
|
* @method
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
static deserialize(serialized) {
|
||||||
|
let {
|
||||||
|
kind,
|
||||||
|
description,
|
||||||
|
details,
|
||||||
|
url,
|
||||||
|
} = JSON.parse(serialized);
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind,
|
||||||
|
description,
|
||||||
|
details,
|
||||||
|
url,
|
||||||
|
ipfsData: serialized,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize object to JSON
|
||||||
|
*
|
||||||
|
* @method
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
static serialize(deserialized) {
|
||||||
|
let {
|
||||||
|
contributorIpfsHash,
|
||||||
|
kind,
|
||||||
|
description,
|
||||||
|
url,
|
||||||
|
} = deserialized;
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
"@context": "https://schema.kosmos.org",
|
||||||
|
"@type": "Contribution",
|
||||||
|
"contributor": {
|
||||||
|
"ipfs": contributorIpfsHash
|
||||||
|
},
|
||||||
|
kind,
|
||||||
|
description,
|
||||||
|
"details": {}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
data["url"] = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write it pretty to ipfs
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-91
@@ -1,103 +1,27 @@
|
|||||||
import Ember from 'ember';
|
import EmberObject from 'ember-object';
|
||||||
|
|
||||||
const {
|
|
||||||
isPresent,
|
|
||||||
isEmpty,
|
|
||||||
} = Ember;
|
|
||||||
|
|
||||||
export default Ember.Object.extend({
|
|
||||||
|
|
||||||
|
export default EmberObject.extend({
|
||||||
|
// Contract
|
||||||
id: null,
|
id: null,
|
||||||
creatorAddress: null,
|
creatorAddress: null,
|
||||||
recipientAddress: null,
|
|
||||||
recipientId: null,
|
recipientId: null,
|
||||||
recipientName: null,
|
amount: null,
|
||||||
recipientProfile: null,
|
|
||||||
votesCount: null,
|
votesCount: null,
|
||||||
votesNeeded: null,
|
votesNeeded: null,
|
||||||
amount: null,
|
|
||||||
executed: null,
|
executed: null,
|
||||||
contribution: null,
|
|
||||||
kind: null,
|
|
||||||
description: null,
|
|
||||||
url: null,
|
|
||||||
details: null,
|
|
||||||
ipfsHash: null,
|
ipfsHash: null,
|
||||||
|
|
||||||
/**
|
// IPFS
|
||||||
* Loads the contribution details from IPFS and sets local instance
|
kind: null,
|
||||||
* properties from it
|
description: null,
|
||||||
*
|
details: {},
|
||||||
* @method
|
url: null,
|
||||||
* @public
|
ipfsData: '',
|
||||||
*/
|
|
||||||
loadContribution(ipfs) {
|
|
||||||
let promise = new Ember.RSVP.Promise((resolve, reject) => {
|
|
||||||
ipfs.getFile(this.get('ipfsHash')).then(content => {
|
|
||||||
let contributionJSON = JSON.parse(content);
|
|
||||||
let contribution = Ember.Object.create(contributionJSON);
|
|
||||||
|
|
||||||
this.setProperties({
|
// Deprecated
|
||||||
kind: contribution.get('kind'),
|
recipientAddress: null,
|
||||||
description: contribution.get('description'),
|
recipientName: null,
|
||||||
url: contribution.get('url')
|
recipientProfile: null,
|
||||||
});
|
|
||||||
|
|
||||||
// TODO load details
|
|
||||||
// let details = profile.get('accounts');
|
|
||||||
|
|
||||||
Ember.Logger.debug('[proposal] loaded contribution details', contributionJSON);
|
|
||||||
resolve();
|
|
||||||
}).catch((err) => {
|
|
||||||
Ember.Logger.error('[proposal] error trying to load contribution details', this.get('ipfsHash'), err);
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a JSON-LD object of the contribution, according to
|
|
||||||
* https://github.com/67P/kosmos-schemas/blob/master/schemas/contribution.json
|
|
||||||
*
|
|
||||||
* @method
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
contributionToJSON() {
|
|
||||||
if (isEmpty(this.get('recipientProfile'))) {
|
|
||||||
throw new Error('IPFS hash for recipient profile missing from proposal object');
|
|
||||||
}
|
|
||||||
if (isEmpty(this.get('kind')) || isEmpty(this.get('description'))) {
|
|
||||||
throw new Error('Missing one or more required properties: kind, description');
|
|
||||||
}
|
|
||||||
|
|
||||||
let contribution = {
|
|
||||||
"@context": "https://schema.kosmos.org",
|
|
||||||
"@type": "Contribution",
|
|
||||||
"contributor": {
|
|
||||||
"ipfs": this.get('recipientProfile')
|
|
||||||
},
|
|
||||||
"kind": this.get('kind'),
|
|
||||||
"description": this.get('description'),
|
|
||||||
"details": {}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isPresent(this.get('url'))) {
|
|
||||||
contribution["url"] = this.get('url');
|
|
||||||
}
|
|
||||||
|
|
||||||
return contribution;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the JSON-LD representation of the contribution as a string
|
|
||||||
*
|
|
||||||
* @method
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
serializeContribution() {
|
|
||||||
return JSON.stringify(this.contributionToJSON());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// TODO: add contributor relation
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,37 +1,15 @@
|
|||||||
import Ember from 'ember';
|
import injectService from 'ember-service/inject';
|
||||||
import Proposal from 'kredits-web/models/proposal';
|
import Route from 'ember-route';
|
||||||
|
|
||||||
const {
|
|
||||||
Route,
|
|
||||||
RSVP,
|
|
||||||
inject: {
|
|
||||||
service
|
|
||||||
}
|
|
||||||
} = Ember;
|
|
||||||
|
|
||||||
export default Route.extend({
|
export default Route.extend({
|
||||||
|
kredits: injectService(),
|
||||||
|
|
||||||
kredits: service(),
|
setupController(controller) {
|
||||||
|
this._super(...arguments);
|
||||||
|
|
||||||
model(params) {
|
this.get('kredits').getContributors()
|
||||||
const proposal = Proposal.create({
|
.then((contributors) => {
|
||||||
recipientAddress: params.recipient,
|
controller.set('contributors', contributors);
|
||||||
amount: params.amount,
|
|
||||||
url: params.url,
|
|
||||||
kind: params.kind || 'dev',
|
|
||||||
ipfsHash: params.ipfsHash
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return RSVP.hash({
|
|
||||||
proposal,
|
|
||||||
contributors: this.get('kredits').getContributors()
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
setupController(controller, model) {
|
|
||||||
this._super(controller, model.proposal);
|
|
||||||
|
|
||||||
controller.set('contributors', model.contributors);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
+75
-47
@@ -8,11 +8,11 @@ import computed, { alias } from 'ember-computed';
|
|||||||
import { isEmpty, isPresent } from 'ember-utils';
|
import { isEmpty, isPresent } from 'ember-utils';
|
||||||
|
|
||||||
import config from 'kredits-web/config/environment';
|
import config from 'kredits-web/config/environment';
|
||||||
import Proposal from 'kredits-web/models/proposal';
|
|
||||||
|
|
||||||
import abis from 'contracts/abis';
|
import abis from 'contracts/abis';
|
||||||
import addresses from 'contracts/addresses';
|
import addresses from 'contracts/addresses';
|
||||||
import {
|
import {
|
||||||
|
ContributionSerializer,
|
||||||
ContributorSerializer,
|
ContributorSerializer,
|
||||||
fromBytes32,
|
fromBytes32,
|
||||||
toBytes32
|
toBytes32
|
||||||
@@ -22,7 +22,6 @@ const {
|
|||||||
getOwner,
|
getOwner,
|
||||||
Logger: {
|
Logger: {
|
||||||
debug,
|
debug,
|
||||||
warn,
|
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
} = Ember;
|
} = Ember;
|
||||||
@@ -82,7 +81,11 @@ export default Service.extend({
|
|||||||
|
|
||||||
registryContract: computed('ethProvider', function() {
|
registryContract: computed('ethProvider', function() {
|
||||||
let networkId = this.get('ethProvider').chainId;
|
let networkId = this.get('ethProvider').chainId;
|
||||||
let registry = new ethers.Contract(addresses['Registry'][networkId], abis['Registry'], this.get('ethProvider'));
|
let registry = new ethers.Contract(
|
||||||
|
addresses['Registry'][networkId],
|
||||||
|
abis['Registry'],
|
||||||
|
this.get('ethProvider')
|
||||||
|
);
|
||||||
return registry;
|
return registry;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -115,6 +118,14 @@ export default Service.extend({
|
|||||||
return contributor;
|
return contributor;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
buildProposal(attributes) {
|
||||||
|
debug('[kredits] buildProposal', attributes);
|
||||||
|
|
||||||
|
let proposal = getOwner(this).lookup('model:proposal');
|
||||||
|
proposal.setProperties(attributes);
|
||||||
|
return proposal;
|
||||||
|
},
|
||||||
|
|
||||||
getContributorById(id) {
|
getContributorById(id) {
|
||||||
return this.get('contributorsContract')
|
return this.get('contributorsContract')
|
||||||
.then((contract) => contract.getContributorById(id))
|
.then((contract) => contract.getContributorById(id))
|
||||||
@@ -124,7 +135,7 @@ export default Service.extend({
|
|||||||
let isCurrentUser = this.get('currentUserAccounts').includes(address);
|
let isCurrentUser = this.get('currentUserAccounts').includes(address);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id: id.toString(),
|
||||||
address,
|
address,
|
||||||
balance: balance.toNumber(),
|
balance: balance.toNumber(),
|
||||||
ipfsHash,
|
ipfsHash,
|
||||||
@@ -191,44 +202,50 @@ export default Service.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getProposalData(i) {
|
getProposalById(id) {
|
||||||
return this.get('kreditsContract')
|
return this.get('kreditsContract')
|
||||||
.then((contract) => contract.proposals(i))
|
.then((contract) => contract.proposals(id))
|
||||||
.then(p => {
|
.then(this.reassembleIpfsHash)
|
||||||
let { ipfsHash: digest, hashFunction, hashSize } = p;
|
// Set basic data
|
||||||
let ipfsHash = fromBytes32({ digest, hashFunction, hashSize });
|
.then(({
|
||||||
|
creator: creatorAddress,
|
||||||
let proposal = Proposal.create({
|
recipientId,
|
||||||
id : i,
|
votesCount,
|
||||||
creatorAddress : p.creator,
|
votesNeeded,
|
||||||
recipientId : p.recipientId.toNumber(),
|
amount,
|
||||||
votesCount : p.votesCount.toNumber(),
|
executed,
|
||||||
votesNeeded : p.votesNeeded.toNumber(),
|
ipfsHash,
|
||||||
amount : p.amount.toNumber(),
|
}) => {
|
||||||
executed : p.executed,
|
return {
|
||||||
|
id: id.toString(),
|
||||||
|
creatorAddress,
|
||||||
|
recipientId: recipientId.toNumber(),
|
||||||
|
votesCount: votesCount.toNumber(),
|
||||||
|
votesNeeded: votesNeeded.toNumber(),
|
||||||
|
amount: amount.toNumber(),
|
||||||
|
executed,
|
||||||
ipfsHash
|
ipfsHash
|
||||||
});
|
};
|
||||||
|
})
|
||||||
if (proposal.get('ipfsHash')) {
|
// Fetch IPFS data if available
|
||||||
// TODO: move ipfs into model
|
.then((data) => {
|
||||||
return proposal
|
return this.fetchAndMergeIpfsData(data, ContributionSerializer);
|
||||||
.loadContribution(this.get('ipfs'))
|
})
|
||||||
.then(() => { return proposal; });
|
.then((attributes) => {
|
||||||
} else {
|
return this.buildProposal(attributes);
|
||||||
warn('[kredits] proposal from blockchain is missing IPFS hash', proposal);
|
|
||||||
return proposal;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getProposals() {
|
getProposals() {
|
||||||
return this.get('kreditsContract')
|
return this.get('kreditsContract')
|
||||||
.then((contract) => contract.proposalsCount())
|
.then((contract) => contract.proposalsCount())
|
||||||
.then(proposalsCount => {
|
.then((count) => {
|
||||||
|
count = count.toNumber();
|
||||||
|
debug('[kredits] proposals count:', count);
|
||||||
let proposals = [];
|
let proposals = [];
|
||||||
|
|
||||||
for(var i = 0; i < proposalsCount.toNumber(); i++) {
|
for(var i = 0; i < count; i++) {
|
||||||
proposals.push(this.getProposalData(i));
|
proposals.push(this.getProposalById(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
return RSVP.all(proposals);
|
return RSVP.all(proposals);
|
||||||
@@ -283,29 +300,40 @@ export default Service.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
addProposal(proposal) {
|
addProposal(attributes) {
|
||||||
const {
|
debug('[kredits] add proposal', attributes);
|
||||||
recipientAddress,
|
|
||||||
amount,
|
let json = ContributionSerializer.serialize(attributes);
|
||||||
url
|
// TODO: validate against schema
|
||||||
} = proposal.getProperties('recipientAddress', 'amount', 'url');
|
|
||||||
|
|
||||||
return this.get('ipfs')
|
return this.get('ipfs')
|
||||||
.storeFile(proposal.serializeContribution())
|
.storeFile(json)
|
||||||
.then(ipfsHash => {
|
// Set ipfsHash
|
||||||
|
.then((ipfsHash) => {
|
||||||
|
delete attributes.contributorIpfsHash;
|
||||||
|
attributes.ipfsHash = ipfsHash;
|
||||||
|
return attributes;
|
||||||
|
})
|
||||||
|
.then((attributes) => {
|
||||||
return this.get('kreditsContract')
|
return this.get('kreditsContract')
|
||||||
.then((contract) => {
|
.then((contract) => {
|
||||||
return contract.addProposal(
|
let { recipientId, amount, ipfsHash } = attributes;
|
||||||
recipientAddress,
|
let { digest, hashFunction, hashSize } = toBytes32(ipfsHash);
|
||||||
|
|
||||||
|
let proposal = [
|
||||||
|
recipientId,
|
||||||
amount,
|
amount,
|
||||||
url,
|
digest,
|
||||||
ipfsHash
|
hashFunction,
|
||||||
);
|
hashSize,
|
||||||
|
];
|
||||||
|
debug('[kredits] addProposal', ...proposal);
|
||||||
|
return contract.addProposal(...proposal);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
debug('[kredits] add proposal response', data);
|
debug('[kredits] add proposal response', data);
|
||||||
return data;
|
return this.buildProposal(attributes);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{add-proposal proposal=model contributors=contributors onSave=(action 'onSave')}}
|
{{add-proposal contributors=minedContributors save=(action 'save')}}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { module, test } from 'ember-qunit';
|
||||||
|
import schemas from 'npm:kosmos-schemas';
|
||||||
|
import tv4 from 'npm:tv4';
|
||||||
|
import { ContributionSerializer } from 'kredits-web/lib/kredits';
|
||||||
|
|
||||||
|
module('Serializers contribution');
|
||||||
|
|
||||||
|
test('#serialize returns a valid JSON-LD representation', function(assert) {
|
||||||
|
let serialized = ContributionSerializer.serialize({
|
||||||
|
recipientAddress: '0xd4a64570b12da659ee4bbd41c3509b7b1f9c51ac',
|
||||||
|
kind: 'design',
|
||||||
|
description: 'New logo design',
|
||||||
|
url: 'http://opensourcedesign.org',
|
||||||
|
contributorIpfsHash: 'QmT2A7rY4e7uoKktkcFHQNN7BD1oXdZTgd8wNkr1u9nNVE'
|
||||||
|
});
|
||||||
|
|
||||||
|
let valid = tv4.validate(JSON.parse(serialized), schemas['contribution']);
|
||||||
|
assert.ok(valid);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#deserialize returns a valid object representation', function(assert) {
|
||||||
|
let json = JSON.stringify({
|
||||||
|
"@context": "https://schema.kosmos.org",
|
||||||
|
"@type": "Contribution",
|
||||||
|
"contributor": {
|
||||||
|
"ipfs": "QmT2A7rY4e7uoKktkcFHQNN7BD1oXdZTgd8wNkr1u9nNVE"
|
||||||
|
},
|
||||||
|
"kind": "design",
|
||||||
|
"description": "New logo design",
|
||||||
|
"details": {},
|
||||||
|
"url": "http://opensourcedesign.org"
|
||||||
|
});
|
||||||
|
let deserialized = ContributionSerializer.deserialize(json);
|
||||||
|
|
||||||
|
let expected = {
|
||||||
|
kind: 'design',
|
||||||
|
description: 'New logo design',
|
||||||
|
details: {},
|
||||||
|
url: 'http://opensourcedesign.org',
|
||||||
|
ipfsData: json,
|
||||||
|
};
|
||||||
|
assert.deepEqual(expected, deserialized);
|
||||||
|
});
|
||||||
@@ -1,49 +1,8 @@
|
|||||||
import { moduleFor, test } from 'ember-qunit';
|
// import { moduleFor, test } from 'ember-qunit';
|
||||||
import schemas from 'npm:kosmos-schemas';
|
//
|
||||||
import tv4 from 'npm:tv4';
|
// moduleFor('model:proposal', 'Unit | Model | proposal');
|
||||||
|
//
|
||||||
moduleFor('model:proposal', 'Unit | Model | proposal');
|
// test('contributor relation', function(assert) {
|
||||||
|
// // TODO: Test contributor relation
|
||||||
test('#toJSON() requires a recipient profile IPFS hash to be set', function(assert) {
|
// assert.ok(true);
|
||||||
let model = this.subject();
|
// });
|
||||||
|
|
||||||
model.setProperties({
|
|
||||||
recipientAddress: '0xd4a64570b12da659ee4bbd41c3509b7b1f9c51ac'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
let json = model.contributionToJSON();
|
|
||||||
assert.notOk(json, true);
|
|
||||||
} catch(e) {
|
|
||||||
assert.ok(e.message.match(/IPFS hash .* missing/i));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('#toJSON() requires kind and description to be set', function(assert) {
|
|
||||||
let model = this.subject();
|
|
||||||
|
|
||||||
model.setProperties({
|
|
||||||
recipientProfile: 'QmT2A7rY4e7uoKktkcFHQNN7BD1oXdZTgd8wNkr1u9nNVE'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
let json = model.contributionToJSON();
|
|
||||||
assert.notOk(json, true);
|
|
||||||
} catch(e) {
|
|
||||||
assert.ok(e.message.match(/Missing .* kind.*description/i));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('#toJSON() returns a valid JSON-LD representation of the model', function(assert) {
|
|
||||||
let model = this.subject();
|
|
||||||
|
|
||||||
model.setProperties({
|
|
||||||
recipientAddress: '0xd4a64570b12da659ee4bbd41c3509b7b1f9c51ac',
|
|
||||||
kind: 'design',
|
|
||||||
description: 'New logo design',
|
|
||||||
url: 'http://opensourcedesign.org',
|
|
||||||
recipientProfile: 'QmT2A7rY4e7uoKktkcFHQNN7BD1oXdZTgd8wNkr1u9nNVE'
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.ok(tv4.validate(model.contributionToJSON(), schemas['contribution']));
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user