Move all metadata to IPFS with proper schemas #16
@@ -1,41 +1,58 @@
|
||||
import Ember from 'ember';
|
||||
|
|
||||
import Contributor from 'kredits-web/models/contributor';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
const {
|
||||
Component,
|
||||
isPresent,
|
||||
inject: {
|
||||
service
|
||||
},
|
||||
computed
|
||||
} = Ember;
|
||||
|
||||
id: null,
|
||||
realName: null,
|
||||
address: null,
|
||||
ipfsHash: null,
|
||||
isCore: false,
|
||||
export default Component.extend({
|
||||
|
||||
kredits: service(),
|
||||
|
||||
newContributor: null,
|
||||
inProgress: false,
|
||||
|
||||
isValidId: function() {
|
||||
return Ember.isPresent(this.get('id'));
|
||||
}.property('id'),
|
||||
|
||||
isValidRealName: function() {
|
||||
return Ember.isPresent(this.get('realName'));
|
||||
}.property('realName'),
|
||||
|
||||
isValidAddress: function() {
|
||||
return this.get('kredits.web3Instance').isAddress(this.get('address'));
|
||||
}.property('address'),
|
||||
return this.get('kredits.web3')
|
||||
.isAddress(this.get('newContributor.address'));
|
||||
}.property('kredits.web3', 'newContributor.address'),
|
||||
|
||||
isValid: function() {
|
||||
return this.get('isValidId') && this.get('isValidRealName') && this.get('isValidAddress');
|
||||
}.property('isValidAddress', 'isValidId', 'isValidRealName'),
|
||||
isValidName: function() {
|
||||
return isPresent(this.get('newContributor.name'));
|
||||
}.property('newContributor.name'),
|
||||
|
||||
isValidURL: function() {
|
||||
return isPresent(this.get('newContributor.url'));
|
||||
}.property('newContributor.url'),
|
||||
|
||||
isValidGithubUID: function() {
|
||||
return isPresent(this.get('newContributor.github_uid'));
|
||||
}.property('newContributor.github_uid'),
|
||||
|
||||
isValidGithubUsername: function() {
|
||||
return isPresent(this.get('newContributor.github_username'));
|
||||
}.property('newContributor.github_username'),
|
||||
|
||||
isValidWikiUsername: function() {
|
||||
return isPresent(this.get('newContributor.wiki_username'));
|
||||
}.property('newContributor.wiki_username'),
|
||||
|
||||
isValid: computed.and(
|
||||
'isValidAddress',
|
||||
'isValidName',
|
||||
|
could we validate these using some json schema validators? could we validate these using some json schema validators?
In theory yes (there's e.g. In theory yes (there's e.g. `"format": "uri"`), but it'd be much easier here to just have some simple regexp for HTTP URIs imo (which also keeps the build size down).
|
||||
'isValidGithubUID'
|
||||
),
|
||||
|
||||
reset: function() {
|
||||
this.setProperties({
|
||||
id: null,
|
||||
realName: null,
|
||||
address: null,
|
||||
ipfsHash: null,
|
||||
isCore: true,
|
||||
newContributor: Contributor.create({ kind: 'person' }),
|
||||
inProgress: false
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
actions: {
|
||||
@@ -49,12 +66,7 @@ export default Ember.Component.extend({
|
||||
if (this.get('isValid')) {
|
||||
this.set('inProgress', true);
|
||||
|
||||
this.get('kredits').addContributor(
|
||||
this.get('address'),
|
||||
this.get('realName'),
|
||||
this.get('isCore'),
|
||||
this.get('id')
|
||||
).then(contributor => {
|
||||
this.get('kredits').addContributor(this.get('newContributor')).then(contributor => {
|
||||
this.reset();
|
||||
this.get('contributors').pushObject(contributor);
|
||||
window.scroll(0,0);
|
||||
|
||||
@@ -1,31 +1,58 @@
|
||||
<form {{action "save" on="submit"}}>
|
||||
<p>
|
||||
{{input type="checkbox" name="is-core" id="is-core" checked=isCore}}
|
||||
{{input type="checkbox" name="is-core" id="is-core" checked=newContributor.isCore}}
|
||||
<label for="is-core" class="checkbox">
|
||||
Core team member (can add contributors)
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
{{input name="id"
|
||||
type="text"
|
||||
placeholder="GitHub UID (123)"
|
||||
value=id
|
||||
class=(if isValidId 'valid' '')}}
|
||||
</p>
|
||||
<p>
|
||||
{{input name="realname"
|
||||
type="text"
|
||||
placeholder="GitHub username"
|
||||
value=realName
|
||||
class=(if isValidRealName 'valid' '')}}
|
||||
</p>
|
||||
<p>
|
||||
{{input name="address"
|
||||
type="text"
|
||||
placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4"
|
||||
value=address
|
||||
value=newContributor.address
|
||||
class=(if isValidAddress 'valid' '')}}
|
||||
</p>
|
||||
<p>
|
||||
<select required onchange={{action (mut newContributor.kind) value="target.value"}}>
|
||||
<option value="person" selected={{eq newContributor.kind "person"}}>Person</option>
|
||||
<option value="organization" selected={{eq newContributor.kind "organization"}}>Organization</option>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
{{input name="name"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value=newContributor.name
|
||||
class=(if isValidName 'valid' '')}}
|
||||
</p>
|
||||
<p>
|
||||
{{input name="url"
|
||||
type="text"
|
||||
placeholder="URL"
|
||||
value=newContributor.url
|
||||
class=(if isValidURL 'valid' '')}}
|
||||
</p>
|
||||
<p>
|
||||
{{input name="github_uid"
|
||||
type="text"
|
||||
placeholder="GitHub UID (123)"
|
||||
value=newContributor.github_uid
|
||||
class=(if isValidGithubUID 'valid' '')}}
|
||||
</p>
|
||||
<p>
|
||||
{{input name="github_username"
|
||||
type="text"
|
||||
placeholder="GitHub username"
|
||||
value=newContributor.github_username
|
||||
class=(if isValidGithubUsername 'valid' '')}}
|
||||
</p>
|
||||
<p>
|
||||
{{input name="wiki_username"
|
||||
type="text"
|
||||
placeholder="Wiki Username"
|
||||
value=newContributor.wiki_username
|
||||
class=(if isValidWikiUsername 'valid' '')}}
|
||||
</p>
|
||||
<p class="actions">
|
||||
{{input type="submit" value=(if inProgress 'Processing' 'Save') disabled=inProgress}}
|
||||
</p>
|
||||
|
||||
@@ -2,6 +2,7 @@ import Ember from 'ember';
|
||||
|
||||
const {
|
||||
Component,
|
||||
isPresent,
|
||||
inject: {
|
||||
service
|
||||
},
|
||||
@@ -13,47 +14,52 @@ export default Component.extend({
|
||||
kredits: service(),
|
||||
|
You should always use a radix parameter of You should always use a radix parameter of `10` with `parseInt`.
What does that mean? What does that mean?
The second parameter of The second parameter of `parseInt` should always be `10` if you want it to be parsed as decimal number. So in this case it should be `parseInt(this.get('proposal.amount'), 10)`.
Otherwise, in certain situations the number will be interpreted with a different base (e.g. when the number starts with a 0, it will be octal).
I see, thanks. I see, thanks.
|
||||
|
||||
proposal: null,
|
||||
contributors: null,
|
||||
inProgress: false,
|
||||
|
||||
isValidRecipient: computed('proposal.recipientAddress', function() {
|
||||
return this.get('kredits.web3Instance').isAddress(this.get('proposal.recipientAddress'));
|
||||
return this.get('kredits.web3').isAddress(this.get('proposal.recipientAddress'));
|
||||
}),
|
||||
|
||||
isValidAmount: computed('proposal.amount', function() {
|
||||
// TODO
|
||||
return true;
|
||||
return parseInt(this.get('proposal.amount'), 10) > 0;
|
||||
}),
|
||||
|
||||
isValidUrl: computed('proposal.url', function() {
|
||||
// TODO
|
||||
return true;
|
||||
return isPresent(this.get('proposal.url'));
|
||||
}),
|
||||
|
Is this supposed to be Is this supposed to be `kredits.web3` now? Because in the `add-contributor` component it's still using `kredits.web3Instance`.
Not "now", but in general, yes. I don't know why it's not in the add-contributor component. Not "now", but in general, yes. I don't know why it's not in the add-contributor component.
|
||||
|
||||
isValidIpfsHash: computed('proposal.ipfsHash', function() {
|
||||
// TODO
|
||||
return true;
|
||||
isValidDescription: computed('proposal.description', function() {
|
||||
return isPresent(this.get('proposal.description'));
|
||||
}),
|
||||
|
||||
isValid: computed.and('isValidRecipient', 'isValidAmount', 'isValidUrl',
|
||||
'isValidIpfsHash'),
|
||||
isValid: computed.and('isValidRecipient',
|
||||
'isValidAmount',
|
||||
'isValidDescription'),
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
if (this.get('isValid')) {
|
||||
this.set('inProgress', true);
|
||||
|
||||
this.get('kredits').addProposal(this.get('proposal'))
|
||||
.then(() => {
|
||||
this.attrs.onSave();
|
||||
}).catch((error) => {
|
||||
Ember.Logger.error('Error creating the proposal', error);
|
||||
alert('Something went wrong.');
|
||||
}).finally(() => {
|
||||
this.set('inProgress', false);
|
||||
});
|
||||
} else {
|
||||
if (! this.get('isValid')) {
|
||||
alert('Invalid data. Please review and try again.');
|
||||
return false;
|
||||
}
|
||||
this.set('inProgress', true);
|
||||
let proposal = this.get('proposal');
|
||||
|
||||
// Set the recipient's IPFS profile hash so it can be used in the
|
||||
// contribution object (which is to be stored in IPFS as well)
|
||||
let contributor = this.get('contributors').findBy('address', proposal.get('recipientAddress'));
|
||||
proposal.set('recipientProfile', contributor.get('ipfsHash'));
|
||||
|
||||
this.get('kredits').addProposal(proposal)
|
||||
.then(() => {
|
||||
this.attrs.onSave();
|
||||
}).catch((error) => {
|
||||
Ember.Logger.error('[add-proposal] error creating the proposal', error);
|
||||
alert('Something went wrong.');
|
||||
}).finally(() => {
|
||||
this.set('inProgress', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
{{/each}}
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<select required onchange={{action (mut proposal.kind) value="target.value"}}>
|
||||
<option value="community" selected={{eq proposal.kind "community"}}>Community</option>
|
||||
<option value="design" selected={{eq proposal.kind "design"}}>Design</option>
|
||||
<option value="dev" selected={{eq proposal.kind "dev"}}>Development</option>
|
||||
<option value="docs" selected={{eq proposal.kind "docs"}}>Documentation</option>
|
||||
<option value="ops" selected={{eq proposal.kind "ops"}}>IT Operations</option>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
{{input type="text"
|
||||
placeholder="100"
|
||||
@@ -15,15 +24,15 @@
|
||||
</p>
|
||||
<p>
|
||||
{{input type="text"
|
||||
placeholder="URL"
|
||||
value=proposal.url
|
||||
class=(if isValidUrl 'valid' '')}}
|
||||
placeholder="Description"
|
||||
value=proposal.description
|
||||
class=(if isValidDescription 'valid' '')}}
|
||||
</p>
|
||||
<p>
|
||||
{{input type="text"
|
||||
placeholder="IPFS Hash"
|
||||
value=proposal.ipfsHash
|
||||
class=(if isValidIpfsHash 'valid' '')}}
|
||||
placeholder="URL (optional)"
|
||||
value=proposal.url
|
||||
class=(if isValidUrl 'valid' '')}}
|
||||
</p>
|
||||
<p class="actions">
|
||||
{{input type="submit" value=(if inProgress 'Processing' 'Save') disabled=inProgress}}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<tr class="{{if contributor.isCurrentUser 'current-user'}}" {{action "toggleContributorInfo" contributor}}>
|
||||
<td class="person">
|
||||
<img class="avatar" src={{contributor.avatarURL}}>
|
||||
{{contributor.github_username}}
|
||||
{{contributor.name}}
|
||||
</td>
|
||||
<td class="kredits">
|
||||
<span class="amount">{{contributor.kredits}}</span>
|
||||
@@ -12,14 +12,12 @@
|
||||
</tr>
|
||||
<tr class="metadata {{if contributor.isCurrentUser 'current-user'}} {{if contributor.showMetadata 'visible'}}">
|
||||
<td colspan="2">
|
||||
<dl>
|
||||
<dt>Ethereum address</dt>
|
||||
<dd><a href="https://testnet.etherscan.io/address/{{contributor.address}}">{{contributor.address}}</a></dd>
|
||||
<ul>
|
||||
<li><a href="https://testnet.etherscan.io/address/{{contributor.address}}">Inspect Ethereum transactions</a></li>
|
||||
{{#if contributor.ipfsHash}}
|
||||
<dt>IPFS profile data</dt>
|
||||
<dd><a href="https://ipfs.io/ipfs/{{contributor.ipfsHash}}">{{contributor.ipfsHash}}</a></dd>
|
||||
<li><a href="https://ipfs.io/ipfs/{{contributor.ipfsHash}}">Inspect IPFS profile</a></li>
|
||||
{{/if}}
|
||||
</dl>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{{#each proposals as |proposal|}}
|
||||
<li>
|
||||
<span class="id">#{{proposal.id}}</span>
|
||||
Issue{{#if proposal.executed}}d{{/if}}
|
||||
<li data-proposal-id={{proposal.id}} title="({{proposal.kind}}) {{proposal.description}}">
|
||||
<span class="category {{proposal.kind}}">♥</span>
|
||||
<span class="amount">{{proposal.amount}}</span><span class="symbol">₭S</span>
|
||||
to <span class="recipient" title="{{proposal.recipientAddress}}">{{proposal.recipientName}}</span>
|
||||
for <span class="recipient">{{proposal.recipientName}}</span>
|
||||
<span class="votes">({{proposal.votesCount}}/{{proposal.votesNeeded}} votes)</span>
|
||||
|
||||
{{#unless proposal.executed}}<button {{action "confirm" proposal.id}}>+1</button>{{/unless}}
|
||||
|
||||
@@ -23,7 +23,7 @@ export default Ember.Controller.extend({
|
||||
let proposals = this.get('model.proposals')
|
||||
.filterBy('executed', false)
|
||||
.map(p => {
|
||||
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).github_username);
|
||||
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).name);
|
||||
return p;
|
||||
});
|
||||
return proposals;
|
||||
@@ -33,7 +33,7 @@ export default Ember.Controller.extend({
|
||||
let proposals = this.get('model.proposals')
|
||||
.filterBy('executed', true)
|
||||
.map(p => {
|
||||
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).github_username);
|
||||
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).name);
|
||||
return p;
|
||||
});
|
||||
return proposals;
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
isPresent,
|
||||
} = Ember;
|
||||
|
||||
export default Ember.Object.extend({
|
||||
|
||||
address: null,
|
||||
name: null,
|
||||
kind: null,
|
||||
url: null,
|
||||
github_username: null,
|
||||
github_uid: null,
|
||||
wiki_username: null,
|
||||
ipfsHash: null,
|
||||
kredits: null,
|
||||
isCore: false,
|
||||
@@ -14,18 +22,98 @@ export default Ember.Object.extend({
|
||||
return `https\:\/\/avatars2.githubusercontent.com/u/${this.get('github_uid')}?v=3&s=128`;
|
||||
}.property('github_uid'),
|
||||
|
||||
serialize() {
|
||||
return JSON.stringify({
|
||||
profiles: {
|
||||
'github.com': {
|
||||
uid: this.get('github_uid'),
|
||||
username: this.get('github_username'),
|
||||
|
||||
/**
|
||||
* Loads the contributor's profile data from IPFS and sets local instance
|
||||
* properties from it
|
||||
*
|
||||
* @method
|
||||
* @public
|
||||
*/
|
||||
loadProfile(ipfs) {
|
||||
let promise = new Ember.RSVP.Promise((resolve, reject) => {
|
||||
ipfs.getFile(this.get('ipfsHash')).then(content => {
|
||||
let profileJSON = JSON.parse(content);
|
||||
let profile = Ember.Object.create(profileJSON);
|
||||
|
||||
this.setProperties({
|
||||
name: profile.get('name'),
|
||||
kind: profile.get('kind')
|
||||
});
|
||||
|
||||
let accounts = profile.get('accounts');
|
||||
let github = accounts.findBy('site', 'github.com');
|
||||
let wiki = accounts.findBy('site', 'wiki.kosmos.org');
|
||||
|
||||
if (isPresent(github)) {
|
||||
this.setProperties({
|
||||
github_username: github.username,
|
||||
github_uid: github.uid,
|
||||
});
|
||||
}
|
||||
// 'wiki.kosmos.org': {
|
||||
// username: this.get('wiki_username')
|
||||
// }
|
||||
}
|
||||
if (isPresent(wiki)) {
|
||||
this.setProperties({
|
||||
wiki_username: wiki.username
|
||||
});
|
||||
}
|
||||
|
||||
Ember.Logger.debug('[contributor] loaded contributor profile', profile);
|
||||
resolve();
|
||||
}).catch((err) => {
|
||||
Ember.Logger.error('[contributor] error trying to load contributor profile', this.get('ipfsHash'), err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a JSON-LD object of the contributor, according to
|
||||
* https://github.com/67P/kosmos-schemas/blob/master/schemas/contributor.json
|
||||
*
|
||||
* @method
|
||||
* @public
|
||||
*/
|
||||
toJSON() {
|
||||
let contributor = {
|
||||
"@context": "https://schema.kosmos.org",
|
||||
"@type": "Contributor",
|
||||
"kind": this.get('kind'),
|
||||
"name": this.get('name'),
|
||||
"accounts": []
|
||||
};
|
||||
|
||||
if (Ember.isPresent(this.get('url'))) {
|
||||
contributor["url"] = this.get('url');
|
||||
}
|
||||
if (Ember.isPresent(this.get('github_uid'))) {
|
||||
contributor.accounts.push({
|
||||
"site": "github.com",
|
||||
"uid": this.get('github_uid'),
|
||||
"username": this.get('github_username'),
|
||||
"url": `https://github.com/${this.get('github_username')}`
|
||||
});
|
||||
}
|
||||
if (Ember.isPresent(this.get('wiki_username'))) {
|
||||
contributor.accounts.push({
|
||||
"site": "wiki.kosmos.org",
|
||||
"username": this.get('wiki_username'),
|
||||
"url": `https://wiki.kosmos.org/User:${this.get('wiki_username')}`
|
||||
});
|
||||
}
|
||||
|
||||
return contributor;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the JSON-LD representation of the model as a string
|
||||
*
|
||||
* @method
|
||||
* @public
|
||||
*/
|
||||
serialize() {
|
||||
return JSON.stringify(this.toJSON());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -1,16 +1,102 @@
|
||||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
isPresent,
|
||||
isEmpty,
|
||||
} = Ember;
|
||||
|
||||
export default Ember.Object.extend({
|
||||
|
||||
id: null,
|
||||
creatorAddress: null,
|
||||
recipientAddress: null,
|
||||
recipientName: null,
|
||||
recipientProfile: null,
|
||||
votesCount: null,
|
||||
votesNeeded: null,
|
||||
amount: null,
|
||||
executed: null,
|
||||
contribution: null,
|
||||
kind: null,
|
||||
description: null,
|
||||
url: null,
|
||||
ipfsHash: null
|
||||
details: null,
|
||||
ipfsHash: null,
|
||||
|
||||
/**
|
||||
* Loads the contribution details from IPFS and sets local instance
|
||||
* properties from it
|
||||
*
|
||||
* @method
|
||||
* @public
|
||||
*/
|
||||
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({
|
||||
kind: contribution.get('kind'),
|
||||
description: contribution.get('description'),
|
||||
url: contribution.get('url')
|
||||
});
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Ember from 'ember';
|
||||
import Contributor from 'kredits-web/models/contributor';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
|
||||
@@ -10,7 +11,8 @@ export default Ember.Route.extend({
|
||||
return Ember.RSVP.hash({
|
||||
contributors: kredits.getContributors(),
|
||||
totalSupply: kredits.getValueFromContract('tokenContract', 'totalSupply'),
|
||||
proposals: kredits.getProposals()
|
||||
proposals: kredits.getProposals(),
|
||||
newContributor: Contributor.create({ kind: 'person' })
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export default Route.extend({
|
||||
recipientAddress: params.recipient,
|
||||
amount: params.amount,
|
||||
url: params.url,
|
||||
kind: params.kind || 'dev',
|
||||
ipfsHash: params.ipfsHash
|
||||
});
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ export default Ember.Service.extend({
|
||||
getFile(hash) {
|
||||
|
I think you want to I think you want to `throw` the error here. Otherwise the `getFile` Promise will resolve with the `err` as value instead of being rejected.
Good catch! Pun indubitably intended. Good catch! Pun indubitably intended.
|
||||
return this.get('ipfs').cat(hash, { buffer: true }).then(res => {
|
||||
return res.toString();
|
||||
}, err => {
|
||||
Ember.Logger.error('[ipfs] error trying to fetch file', hash, err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import config from 'kredits-web/config/environment';
|
||||
import Contributor from 'kredits-web/models/contributor';
|
||||
import Proposal from 'kredits-web/models/proposal';
|
||||
import kreditsContracts from 'npm:kredits-contracts';
|
||||
import uuid from 'npm:uuid';
|
||||
|
||||
const {
|
||||
Service,
|
||||
@@ -101,17 +102,19 @@ export default Service.extend({
|
||||
this.getValueFromContract('kreditsContract', 'contributorAddresses', i).then(address => {
|
||||
this.getValueFromContract('kreditsContract', 'contributors', address).then(person => {
|
||||
this.getValueFromContract('tokenContract', 'balanceOf', address).then(balance => {
|
||||
Ember.Logger.debug('person', person);
|
||||
Ember.Logger.debug('[kredits] person', address, person);
|
||||
|
||||
let contributor = Contributor.create({
|
||||
address: address,
|
||||
github_username: person[1],
|
||||
github_uid: person[0],
|
||||
ipfsHash: person[2],
|
||||
kredits: balance.toNumber(),
|
||||
isCurrentUser: this.get('currentUserAccounts').includes(address)
|
||||
});
|
||||
Ember.Logger.debug('[kredits] contributor', contributor);
|
||||
resolve(contributor);
|
||||
|
||||
contributor.loadProfile(this.get('ipfs')).then(
|
||||
() => resolve(contributor),
|
||||
err => reject(err)
|
||||
);
|
||||
});
|
||||
});
|
||||
}).catch(err => reject(err));
|
||||
@@ -145,8 +148,16 @@ export default Service.extend({
|
||||
url : p[6],
|
||||
ipfsHash : p[7]
|
||||
});
|
||||
Ember.Logger.debug('[kredits] proposal', proposal);
|
||||
resolve(proposal);
|
||||
|
||||
if (proposal.get('ipfsHash')) {
|
||||
proposal.loadContribution(this.get('ipfs')).then(
|
||||
() => resolve(proposal),
|
||||
err => reject(err)
|
||||
);
|
||||
} else {
|
||||
Ember.Logger.warn('[kredits] proposal from blockchain is missing IPFS hash', proposal);
|
||||
resolve(proposal);
|
||||
}
|
||||
}).catch(err => reject(err));
|
||||
});
|
||||
return promise;
|
||||
@@ -175,23 +186,20 @@ export default Service.extend({
|
||||
});
|
||||
},
|
||||
|
||||
addContributor(address, name, isCore, id) {
|
||||
Ember.Logger.debug('[kredits] add contributor', name, address);
|
||||
addContributor(contributor) {
|
||||
Ember.Logger.debug('[kredits] add contributor', contributor);
|
||||
|
||||
let contributor = Contributor.create({
|
||||
address: address,
|
||||
github_username: name,
|
||||
github_uid: id,
|
||||
contributor.setProperties({
|
||||
kredits: 0,
|
||||
isCore: isCore,
|
||||
isCurrentUser: this.get('currentUserAccounts').includes(address)
|
||||
isCurrentUser: this.get('currentUserAccounts').includes(contributor.address)
|
||||
});
|
||||
|
||||
let id = uuid.v4();
|
||||
|
||||
return new Ember.RSVP.Promise((resolve, reject) => {
|
||||
this.get('ipfs').storeFile(contributor.serialize()).then(ipfsHash => {
|
||||
contributor.set('ipfsHash', ipfsHash);
|
||||
Ember.Logger.debug('ADD', address, name, ipfsHash, isCore, id);
|
||||
this.get('kreditsContract').addContributor(address, name, ipfsHash, isCore, id, (err, data) => {
|
||||
this.get('kreditsContract').addContributor(contributor.address, contributor.name, contributor.ipfsHash, contributor.isCore, id, (err, data) => {
|
||||
if (err) { reject(err); return; }
|
||||
Ember.Logger.debug('[kredits] add contributor response', data);
|
||||
resolve(contributor);
|
||||
@@ -205,14 +213,15 @@ export default Service.extend({
|
||||
const {
|
||||
recipientAddress,
|
||||
amount,
|
||||
url,
|
||||
ipfsHash
|
||||
} = proposal.getProperties('recipientAddress', 'amount', 'url', 'ipfsHash');
|
||||
url
|
||||
} = proposal.getProperties('recipientAddress', 'amount', 'url');
|
||||
|
||||
this.get('kreditsContract').addProposal(recipientAddress, amount, url, ipfsHash, (err, data) => {
|
||||
if (err) { reject(err); return; }
|
||||
Ember.Logger.debug('[kredits] add proposal response', data);
|
||||
resolve();
|
||||
this.get('ipfs').storeFile(proposal.serializeContribution()).then(ipfsHash => {
|
||||
this.get('kreditsContract').addProposal(recipientAddress, amount, url, ipfsHash, (err, data) => {
|
||||
if (err) { reject(err); return; }
|
||||
Ember.Logger.debug('[kredits] add proposal response', data);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
$blue: #68d7fb;
|
||||
$purple: #8f68fb;
|
||||
$pink: #e068fb;
|
||||
$green: #97fb68;
|
||||
$yellow: #fbe468;
|
||||
$red: #fb6868;
|
||||
|
||||
$primaryColor: $blue;
|
||||
@@ -15,6 +15,7 @@
|
||||
@import "neat";
|
||||
@import "settings/breakpoints";
|
||||
@import "layout";
|
||||
@import "colors";
|
||||
|
||||
$font-family-sans: 'Open Sans', sans-serif;
|
||||
|
||||
@@ -23,7 +24,7 @@ body {
|
||||
background-repeat: none;
|
||||
background-attachment: fixed;
|
||||
font-family: $font-family-sans;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
color: #fff;
|
||||
}
|
||||
@@ -43,7 +44,7 @@ h1, h2, h3, h4, h5, input, button {
|
||||
section {
|
||||
h2 {
|
||||
font-size: 2.8rem;
|
||||
color: lightblue;
|
||||
color: $primaryColor;
|
||||
@include media($mobile) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
@@ -54,7 +55,7 @@ section {
|
||||
p.stats {
|
||||
padding-top: 3rem;
|
||||
font-size: 1rem;
|
||||
color: lightblue;
|
||||
color: white;
|
||||
text-align: center;
|
||||
span.number {
|
||||
font-weight: 600;
|
||||
@@ -70,14 +71,14 @@ section {
|
||||
.actions {
|
||||
padding-top: 3rem;
|
||||
font-size: 1rem;
|
||||
color: lightblue;
|
||||
color: $primaryColor;
|
||||
text-align: center;
|
||||
@include media($mobile) {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: lightblue;
|
||||
color: $primaryColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +88,7 @@ button, input[type=submit] {
|
||||
display: inline-block;
|
||||
border: 1px solid rgba(22, 21, 40, 1);
|
||||
background-color: rgba(22, 21, 40, 0.6);
|
||||
color: lightblue;
|
||||
color: $primaryColor;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
|
||||
@@ -9,7 +9,7 @@ section#add-contributor, section#add-proposal {
|
||||
padding-top: 1rem;
|
||||
text-align: center;
|
||||
a {
|
||||
color: lightblue;
|
||||
color: $primaryColor;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,54 +15,40 @@ table.contributor-list {
|
||||
|
||||
&.metadata {
|
||||
height: 0;
|
||||
transition: all 0.2s ease-out;
|
||||
|
||||
td {
|
||||
padding: 0 1.2rem;
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
|
||||
a {
|
||||
color: lightblue;
|
||||
color: $primaryColor;
|
||||
&:hover, &:active {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
dl {
|
||||
ul {
|
||||
list-style: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
dt, dd {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
line-height: 1.6rem;
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
dt {
|
||||
clear: both;
|
||||
float: left;
|
||||
width: 30%;
|
||||
}
|
||||
dd {
|
||||
float: right;
|
||||
width: 70%;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
transition: all 0.2s ease-out;
|
||||
|
||||
&.visible {
|
||||
td {
|
||||
padding: 1rem 1.2rem;
|
||||
}
|
||||
|
||||
dl {
|
||||
dt, dd {
|
||||
height: auto;
|
||||
li {
|
||||
display: inline;
|
||||
&+li {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.visible {
|
||||
height: auto;
|
||||
ul {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
margin-top: 12rem;
|
||||
text-align: center;
|
||||
font-size: 1.4rem;
|
||||
color: lightblue;
|
||||
color: $primaryColor;
|
||||
|
||||
@include media($mobile) {
|
||||
margin-top: 6rem;
|
||||
@@ -23,7 +23,7 @@
|
||||
margin-bottom: 2rem;
|
||||
|
||||
#path-comet {
|
||||
fill: lightblue;
|
||||
fill: $primaryColor;
|
||||
opacity: 0.1;
|
||||
|
||||
animation-name: pulse;
|
||||
|
||||
@@ -14,9 +14,14 @@ ul.proposal-list {
|
||||
border-top: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.id {
|
||||
color: lightblue;
|
||||
.category {
|
||||
color: $blue;
|
||||
padding-right: 0.2rem;
|
||||
&.community { color: $red; }
|
||||
&.dev { color: $pink; }
|
||||
&.design { color: $yellow; }
|
||||
&.docs { color: $green; }
|
||||
&.ops { color: $purple; }
|
||||
}
|
||||
.amount {
|
||||
font-weight: 500;
|
||||
@@ -31,8 +36,8 @@ ul.proposal-list {
|
||||
}
|
||||
.votes {
|
||||
font-size: 1rem;
|
||||
color: lightblue;
|
||||
padding-left: 1rem;
|
||||
color: $primaryColor;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
{{add-contributor kredits=kredits
|
||||
contributors=model.contributors
|
||||
{{add-contributor contributors=model.contributors
|
||||
newContributor=model.newContributor
|
||||
contractInteractionEnabled=contractInteractionEnabled}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -23,6 +23,7 @@ module.exports = function(environment) {
|
||||
},
|
||||
|
||||
browserify: {
|
||||
tests: true,
|
||||
transform: [
|
||||
["babelify", {
|
||||
presets: ["es2015"],
|
||||
@@ -38,7 +39,7 @@ module.exports = function(environment) {
|
||||
ipfs: {
|
||||
host: 'ipfs.kosmos.org',
|
||||
port: '5444',
|
||||
protocol: 'http'
|
||||
protocol: 'https'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "1.0.0",
|
||||
"description": "Contribution dashboard of the Kosmos project",
|
||||
"license": "MIT",
|
||||
"author": "Kosmos Community",
|
||||
"author": "Kosmos Contributors <mail@kosmos.org>",
|
||||
"contributors": [
|
||||
"Garret Alfert <alfert@wevelop.de>",
|
||||
"Sebastian Kippe <sebastian@kip.pe>",
|
||||
@@ -55,8 +55,11 @@
|
||||
"ember-resolver": "^2.0.3",
|
||||
"ember-truth-helpers": "1.3.0",
|
||||
"ipfs-api": "^12.1.7",
|
||||
"kosmos-schemas": "~1.1.1",
|
||||
"kredits-contracts": "^2.4.0",
|
||||
"loader.js": "^4.0.10",
|
||||
"tv4": "^1.3.0",
|
||||
"uuid": "^3.0.1",
|
||||
"web3": "^0.18.2"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import { moduleFor, test } from 'ember-qunit';
|
||||
import schemas from 'npm:kosmos-schemas';
|
||||
import tv4 from 'npm:tv4';
|
||||
|
||||
moduleFor('model:contributor', 'Unit | Model | contributor');
|
||||
|
||||
test('avatarURL returns correct URL', function(assert) {
|
||||
test('#avatarURL() returns correct URL', function(assert) {
|
||||
let model = this.subject();
|
||||
model.set('github_uid', '318');
|
||||
|
||||
assert.equal(model.get('avatarURL'), 'https://avatars2.githubusercontent.com/u/318?v=3&s=128');
|
||||
});
|
||||
|
||||
test('#toJSON() returns a valid JSON-LD representation of the model', function(assert) {
|
||||
let model = this.subject();
|
||||
|
||||
model.setProperties({
|
||||
name: 'Satoshi Nakamoto',
|
||||
kind: 'person',
|
||||
github_uid: 123,
|
||||
github_username: 'therealsatoshi',
|
||||
wiki_username: 'Satoshi',
|
||||
});
|
||||
|
||||
assert.ok(tv4.validate(model.toJSON(), schemas['contributor']));
|
||||
});
|
||||
|
||||
@@ -1,9 +1,49 @@
|
||||
import { moduleFor, test } from 'ember-qunit';
|
||||
import schemas from 'npm:kosmos-schemas';
|
||||
import tv4 from 'npm:tv4';
|
||||
|
||||
moduleFor('model:proposal', 'Unit | Model | proposal');
|
||||
|
||||
test('it exists', function(assert) {
|
||||
test('#toJSON() requires a recipient profile IPFS hash to be set', function(assert) {
|
||||
let model = this.subject();
|
||||
// let store = this.store();
|
||||
assert.ok(!!model);
|
||||
|
||||
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']));
|
||||
});
|
||||
|
||||
I think this is supposed to be
newContributor.urlinstead ofnewContributor.name.Not sure about this, but maybe it makes sense to add
kredits.web3Instanceas dependent key as well? So the property gets recalculated whenkredits.web3Instancechanges. Thinking of that race condition where the user provided web3 is only available after a while.I think you missed this one.