Merge pull request #16 from 67P/feature/ipfs-metadata
Move all metadata to IPFS with proper schemas
This commit was merged in pull request #16.
This commit is contained in:
@@ -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',
|
||||
'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(),
|
||||
|
||||
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'));
|
||||
}),
|
||||
|
||||
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;
|
||||
|
||||
+98
-10
@@ -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());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
+87
-1
@@ -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());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
+3
-1
@@ -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) {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+33
-24
@@ -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;
|
||||
+7
-6
@@ -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'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+4
-1
@@ -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']));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user