Move all metadata to IPFS with proper schemas #16
@@ -1,41 +1,58 @@
|
|||||||
import Ember from 'ember';
|
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,
|
export default Component.extend({
|
||||||
realName: null,
|
|
||||||
address: null,
|
|
||||||
ipfsHash: null,
|
|
||||||
isCore: false,
|
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
|
||||||
|
newContributor: null,
|
||||||
inProgress: false,
|
inProgress: false,
|
||||||
|
|
||||||
isValidId: function() {
|
|
||||||
return Ember.isPresent(this.get('id'));
|
|
||||||
}.property('id'),
|
|
||||||
|
|
||||||
isValidRealName: function() {
|
|
||||||
return Ember.isPresent(this.get('realName'));
|
|
||||||
}.property('realName'),
|
|
||||||
|
|
||||||
isValidAddress: function() {
|
isValidAddress: function() {
|
||||||
return this.get('kredits.web3Instance').isAddress(this.get('address'));
|
return this.get('kredits.web3')
|
||||||
}.property('address'),
|
.isAddress(this.get('newContributor.address'));
|
||||||
|
}.property('kredits.web3', 'newContributor.address'),
|
||||||
|
|
||||||
isValid: function() {
|
isValidName: function() {
|
||||||
return this.get('isValidId') && this.get('isValidRealName') && this.get('isValidAddress');
|
return isPresent(this.get('newContributor.name'));
|
||||||
}.property('isValidAddress', 'isValidId', 'isValidRealName'),
|
}.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() {
|
reset: function() {
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
id: null,
|
newContributor: Contributor.create({ kind: 'person' }),
|
||||||
realName: null,
|
|
||||||
address: null,
|
|
||||||
ipfsHash: null,
|
|
||||||
isCore: true,
|
|
||||||
inProgress: false
|
inProgress: false
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -49,12 +66,7 @@ export default Ember.Component.extend({
|
|||||||
if (this.get('isValid')) {
|
if (this.get('isValid')) {
|
||||||
this.set('inProgress', true);
|
this.set('inProgress', true);
|
||||||
|
|
||||||
this.get('kredits').addContributor(
|
this.get('kredits').addContributor(this.get('newContributor')).then(contributor => {
|
||||||
this.get('address'),
|
|
||||||
this.get('realName'),
|
|
||||||
this.get('isCore'),
|
|
||||||
this.get('id')
|
|
||||||
).then(contributor => {
|
|
||||||
this.reset();
|
this.reset();
|
||||||
this.get('contributors').pushObject(contributor);
|
this.get('contributors').pushObject(contributor);
|
||||||
window.scroll(0,0);
|
window.scroll(0,0);
|
||||||
|
|||||||
@@ -1,31 +1,58 @@
|
|||||||
<form {{action "save" on="submit"}}>
|
<form {{action "save" on="submit"}}>
|
||||||
<p>
|
<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">
|
<label for="is-core" class="checkbox">
|
||||||
Core team member (can add contributors)
|
Core team member (can add contributors)
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</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>
|
<p>
|
||||||
{{input name="address"
|
{{input name="address"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4"
|
placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4"
|
||||||
value=address
|
value=newContributor.address
|
||||||
class=(if isValidAddress 'valid' '')}}
|
class=(if isValidAddress 'valid' '')}}
|
||||||
</p>
|
</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">
|
<p class="actions">
|
||||||
{{input type="submit" value=(if inProgress 'Processing' 'Save') disabled=inProgress}}
|
{{input type="submit" value=(if inProgress 'Processing' 'Save') disabled=inProgress}}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Ember from 'ember';
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
Component,
|
Component,
|
||||||
|
isPresent,
|
||||||
inject: {
|
inject: {
|
||||||
service
|
service
|
||||||
},
|
},
|
||||||
@@ -13,47 +14,52 @@ export default Component.extend({
|
|||||||
kredits: service(),
|
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,
|
proposal: null,
|
||||||
|
contributors: null,
|
||||||
inProgress: false,
|
inProgress: false,
|
||||||
|
|
||||||
isValidRecipient: computed('proposal.recipientAddress', function() {
|
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() {
|
isValidAmount: computed('proposal.amount', function() {
|
||||||
// TODO
|
return parseInt(this.get('proposal.amount'), 10) > 0;
|
||||||
return true;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
isValidUrl: computed('proposal.url', function() {
|
isValidUrl: computed('proposal.url', function() {
|
||||||
// TODO
|
return isPresent(this.get('proposal.url'));
|
||||||
return true;
|
|
||||||
}),
|
}),
|
||||||
|
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() {
|
isValidDescription: computed('proposal.description', function() {
|
||||||
// TODO
|
return isPresent(this.get('proposal.description'));
|
||||||
return true;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
isValid: computed.and('isValidRecipient', 'isValidAmount', 'isValidUrl',
|
isValid: computed.and('isValidRecipient',
|
||||||
'isValidIpfsHash'),
|
'isValidAmount',
|
||||||
|
'isValidDescription'),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
save() {
|
save() {
|
||||||
if (this.get('isValid')) {
|
if (! this.get('isValid')) {
|
||||||
|
alert('Invalid data. Please review and try again.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
this.set('inProgress', true);
|
this.set('inProgress', true);
|
||||||
|
let proposal = this.get('proposal');
|
||||||
|
|
||||||
this.get('kredits').addProposal(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(() => {
|
.then(() => {
|
||||||
this.attrs.onSave();
|
this.attrs.onSave();
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
Ember.Logger.error('Error creating the proposal', error);
|
Ember.Logger.error('[add-proposal] error creating the proposal', error);
|
||||||
alert('Something went wrong.');
|
alert('Something went wrong.');
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.set('inProgress', false);
|
this.set('inProgress', false);
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
alert('Invalid data. Please review and try again.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,15 @@
|
|||||||
{{/each}}
|
{{/each}}
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</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>
|
<p>
|
||||||
{{input type="text"
|
{{input type="text"
|
||||||
placeholder="100"
|
placeholder="100"
|
||||||
@@ -15,15 +24,15 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{input type="text"
|
{{input type="text"
|
||||||
placeholder="URL"
|
placeholder="Description"
|
||||||
value=proposal.url
|
value=proposal.description
|
||||||
class=(if isValidUrl 'valid' '')}}
|
class=(if isValidDescription 'valid' '')}}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{input type="text"
|
{{input type="text"
|
||||||
placeholder="IPFS Hash"
|
placeholder="URL (optional)"
|
||||||
value=proposal.ipfsHash
|
value=proposal.url
|
||||||
class=(if isValidIpfsHash '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" value=(if inProgress 'Processing' 'Save') disabled=inProgress}}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<tr class="{{if contributor.isCurrentUser 'current-user'}}" {{action "toggleContributorInfo" contributor}}>
|
<tr class="{{if contributor.isCurrentUser 'current-user'}}" {{action "toggleContributorInfo" contributor}}>
|
||||||
<td class="person">
|
<td class="person">
|
||||||
<img class="avatar" src={{contributor.avatarURL}}>
|
<img class="avatar" src={{contributor.avatarURL}}>
|
||||||
{{contributor.github_username}}
|
{{contributor.name}}
|
||||||
</td>
|
</td>
|
||||||
<td class="kredits">
|
<td class="kredits">
|
||||||
<span class="amount">{{contributor.kredits}}</span>
|
<span class="amount">{{contributor.kredits}}</span>
|
||||||
@@ -12,14 +12,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr class="metadata {{if contributor.isCurrentUser 'current-user'}} {{if contributor.showMetadata 'visible'}}">
|
<tr class="metadata {{if contributor.isCurrentUser 'current-user'}} {{if contributor.showMetadata 'visible'}}">
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<dl>
|
<ul>
|
||||||
<dt>Ethereum address</dt>
|
<li><a href="https://testnet.etherscan.io/address/{{contributor.address}}">Inspect Ethereum transactions</a></li>
|
||||||
<dd><a href="https://testnet.etherscan.io/address/{{contributor.address}}">{{contributor.address}}</a></dd>
|
|
||||||
{{#if contributor.ipfsHash}}
|
{{#if contributor.ipfsHash}}
|
||||||
<dt>IPFS profile data</dt>
|
<li><a href="https://ipfs.io/ipfs/{{contributor.ipfsHash}}">Inspect IPFS profile</a></li>
|
||||||
<dd><a href="https://ipfs.io/ipfs/{{contributor.ipfsHash}}">{{contributor.ipfsHash}}</a></dd>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</dl>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
{{#each proposals as |proposal|}}
|
{{#each proposals as |proposal|}}
|
||||||
<li>
|
<li data-proposal-id={{proposal.id}} title="({{proposal.kind}}) {{proposal.description}}">
|
||||||
<span class="id">#{{proposal.id}}</span>
|
<span class="category {{proposal.kind}}">♥</span>
|
||||||
Issue{{#if proposal.executed}}d{{/if}}
|
|
||||||
<span class="amount">{{proposal.amount}}</span><span class="symbol">₭S</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>
|
<span class="votes">({{proposal.votesCount}}/{{proposal.votesNeeded}} votes)</span>
|
||||||
|
|
||||||
{{#unless proposal.executed}}<button {{action "confirm" proposal.id}}>+1</button>{{/unless}}
|
{{#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')
|
let proposals = this.get('model.proposals')
|
||||||
.filterBy('executed', false)
|
.filterBy('executed', false)
|
||||||
.map(p => {
|
.map(p => {
|
||||||
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).github_username);
|
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).name);
|
||||||
return p;
|
return p;
|
||||||
});
|
});
|
||||||
return proposals;
|
return proposals;
|
||||||
@@ -33,7 +33,7 @@ export default Ember.Controller.extend({
|
|||||||
let proposals = this.get('model.proposals')
|
let proposals = this.get('model.proposals')
|
||||||
.filterBy('executed', true)
|
.filterBy('executed', true)
|
||||||
.map(p => {
|
.map(p => {
|
||||||
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).github_username);
|
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).name);
|
||||||
return p;
|
return p;
|
||||||
});
|
});
|
||||||
return proposals;
|
return proposals;
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
|
|
||||||
|
const {
|
||||||
|
isPresent,
|
||||||
|
} = Ember;
|
||||||
|
|
||||||
export default Ember.Object.extend({
|
export default Ember.Object.extend({
|
||||||
|
|
||||||
address: null,
|
address: null,
|
||||||
|
name: null,
|
||||||
|
kind: null,
|
||||||
|
url: null,
|
||||||
github_username: null,
|
github_username: null,
|
||||||
github_uid: null,
|
github_uid: null,
|
||||||
|
wiki_username: null,
|
||||||
ipfsHash: null,
|
ipfsHash: null,
|
||||||
kredits: null,
|
kredits: null,
|
||||||
isCore: false,
|
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`;
|
return `https\:\/\/avatars2.githubusercontent.com/u/${this.get('github_uid')}?v=3&s=128`;
|
||||||
}.property('github_uid'),
|
}.property('github_uid'),
|
||||||
|
|
||||||
serialize() {
|
|
||||||
return JSON.stringify({
|
/**
|
||||||
profiles: {
|
* Loads the contributor's profile data from IPFS and sets local instance
|
||||||
'github.com': {
|
* properties from it
|
||||||
uid: this.get('github_uid'),
|
*
|
||||||
username: this.get('github_username'),
|
* @method
|
||||||
}
|
* @public
|
||||||
// 'wiki.kosmos.org': {
|
*/
|
||||||
// username: this.get('wiki_username')
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
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';
|
import Ember from 'ember';
|
||||||
|
|
||||||
|
const {
|
||||||
|
isPresent,
|
||||||
|
isEmpty,
|
||||||
|
} = Ember;
|
||||||
|
|
||||||
export default Ember.Object.extend({
|
export default Ember.Object.extend({
|
||||||
|
|
||||||
id: null,
|
id: null,
|
||||||
creatorAddress: null,
|
creatorAddress: null,
|
||||||
recipientAddress: null,
|
recipientAddress: null,
|
||||||
recipientName: null,
|
recipientName: null,
|
||||||
|
recipientProfile: null,
|
||||||
votesCount: null,
|
votesCount: null,
|
||||||
votesNeeded: null,
|
votesNeeded: null,
|
||||||
amount: null,
|
amount: null,
|
||||||
executed: null,
|
executed: null,
|
||||||
|
contribution: null,
|
||||||
|
kind: null,
|
||||||
|
description: null,
|
||||||
url: 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 Ember from 'ember';
|
||||||
|
import Contributor from 'kredits-web/models/contributor';
|
||||||
|
|
||||||
export default Ember.Route.extend({
|
export default Ember.Route.extend({
|
||||||
|
|
||||||
@@ -10,7 +11,8 @@ export default Ember.Route.extend({
|
|||||||
return Ember.RSVP.hash({
|
return Ember.RSVP.hash({
|
||||||
contributors: kredits.getContributors(),
|
contributors: kredits.getContributors(),
|
||||||
totalSupply: kredits.getValueFromContract('tokenContract', 'totalSupply'),
|
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,
|
recipientAddress: params.recipient,
|
||||||
amount: params.amount,
|
amount: params.amount,
|
||||||
url: params.url,
|
url: params.url,
|
||||||
|
kind: params.kind || 'dev',
|
||||||
ipfsHash: params.ipfsHash
|
ipfsHash: params.ipfsHash
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ export default Ember.Service.extend({
|
|||||||
getFile(hash) {
|
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 this.get('ipfs').cat(hash, { buffer: true }).then(res => {
|
||||||
return res.toString();
|
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 Contributor from 'kredits-web/models/contributor';
|
||||||
import Proposal from 'kredits-web/models/proposal';
|
import Proposal from 'kredits-web/models/proposal';
|
||||||
import kreditsContracts from 'npm:kredits-contracts';
|
import kreditsContracts from 'npm:kredits-contracts';
|
||||||
|
import uuid from 'npm:uuid';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
Service,
|
Service,
|
||||||
@@ -101,17 +102,19 @@ export default Service.extend({
|
|||||||
this.getValueFromContract('kreditsContract', 'contributorAddresses', i).then(address => {
|
this.getValueFromContract('kreditsContract', 'contributorAddresses', i).then(address => {
|
||||||
this.getValueFromContract('kreditsContract', 'contributors', address).then(person => {
|
this.getValueFromContract('kreditsContract', 'contributors', address).then(person => {
|
||||||
this.getValueFromContract('tokenContract', 'balanceOf', address).then(balance => {
|
this.getValueFromContract('tokenContract', 'balanceOf', address).then(balance => {
|
||||||
Ember.Logger.debug('person', person);
|
Ember.Logger.debug('[kredits] person', address, person);
|
||||||
|
|
||||||
let contributor = Contributor.create({
|
let contributor = Contributor.create({
|
||||||
address: address,
|
address: address,
|
||||||
github_username: person[1],
|
|
||||||
github_uid: person[0],
|
|
||||||
ipfsHash: person[2],
|
ipfsHash: person[2],
|
||||||
kredits: balance.toNumber(),
|
kredits: balance.toNumber(),
|
||||||
isCurrentUser: this.get('currentUserAccounts').includes(address)
|
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));
|
}).catch(err => reject(err));
|
||||||
@@ -145,8 +148,16 @@ export default Service.extend({
|
|||||||
url : p[6],
|
url : p[6],
|
||||||
ipfsHash : p[7]
|
ipfsHash : p[7]
|
||||||
});
|
});
|
||||||
Ember.Logger.debug('[kredits] proposal', 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);
|
resolve(proposal);
|
||||||
|
}
|
||||||
}).catch(err => reject(err));
|
}).catch(err => reject(err));
|
||||||
});
|
});
|
||||||
return promise;
|
return promise;
|
||||||
@@ -175,23 +186,20 @@ export default Service.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
addContributor(address, name, isCore, id) {
|
addContributor(contributor) {
|
||||||
Ember.Logger.debug('[kredits] add contributor', name, address);
|
Ember.Logger.debug('[kredits] add contributor', contributor);
|
||||||
|
|
||||||
let contributor = Contributor.create({
|
contributor.setProperties({
|
||||||
address: address,
|
|
||||||
github_username: name,
|
|
||||||
github_uid: id,
|
|
||||||
kredits: 0,
|
kredits: 0,
|
||||||
isCore: isCore,
|
isCurrentUser: this.get('currentUserAccounts').includes(contributor.address)
|
||||||
isCurrentUser: this.get('currentUserAccounts').includes(address)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let id = uuid.v4();
|
||||||
|
|
||||||
return new Ember.RSVP.Promise((resolve, reject) => {
|
return new Ember.RSVP.Promise((resolve, reject) => {
|
||||||
this.get('ipfs').storeFile(contributor.serialize()).then(ipfsHash => {
|
this.get('ipfs').storeFile(contributor.serialize()).then(ipfsHash => {
|
||||||
contributor.set('ipfsHash', ipfsHash);
|
contributor.set('ipfsHash', ipfsHash);
|
||||||
Ember.Logger.debug('ADD', address, name, ipfsHash, isCore, id);
|
this.get('kreditsContract').addContributor(contributor.address, contributor.name, contributor.ipfsHash, contributor.isCore, id, (err, data) => {
|
||||||
this.get('kreditsContract').addContributor(address, name, ipfsHash, isCore, id, (err, data) => {
|
|
||||||
if (err) { reject(err); return; }
|
if (err) { reject(err); return; }
|
||||||
Ember.Logger.debug('[kredits] add contributor response', data);
|
Ember.Logger.debug('[kredits] add contributor response', data);
|
||||||
resolve(contributor);
|
resolve(contributor);
|
||||||
@@ -205,16 +213,17 @@ export default Service.extend({
|
|||||||
const {
|
const {
|
||||||
recipientAddress,
|
recipientAddress,
|
||||||
amount,
|
amount,
|
||||||
url,
|
url
|
||||||
ipfsHash
|
} = proposal.getProperties('recipientAddress', 'amount', 'url');
|
||||||
} = proposal.getProperties('recipientAddress', 'amount', 'url', 'ipfsHash');
|
|
||||||
|
|
||||||
|
this.get('ipfs').storeFile(proposal.serializeContribution()).then(ipfsHash => {
|
||||||
this.get('kreditsContract').addProposal(recipientAddress, amount, url, ipfsHash, (err, data) => {
|
this.get('kreditsContract').addProposal(recipientAddress, amount, url, ipfsHash, (err, data) => {
|
||||||
if (err) { reject(err); return; }
|
if (err) { reject(err); return; }
|
||||||
Ember.Logger.debug('[kredits] add proposal response', data);
|
Ember.Logger.debug('[kredits] add proposal response', data);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// logKreditsContract: function() {
|
// logKreditsContract: function() {
|
||||||
|
|||||||
@@ -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 "neat";
|
||||||
@import "settings/breakpoints";
|
@import "settings/breakpoints";
|
||||||
@import "layout";
|
@import "layout";
|
||||||
|
@import "colors";
|
||||||
|
|
||||||
$font-family-sans: 'Open Sans', sans-serif;
|
$font-family-sans: 'Open Sans', sans-serif;
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ body {
|
|||||||
background-repeat: none;
|
background-repeat: none;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
font-family: $font-family-sans;
|
font-family: $font-family-sans;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
@@ -43,7 +44,7 @@ h1, h2, h3, h4, h5, input, button {
|
|||||||
section {
|
section {
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 2.8rem;
|
font-size: 2.8rem;
|
||||||
color: lightblue;
|
color: $primaryColor;
|
||||||
@include media($mobile) {
|
@include media($mobile) {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
@@ -54,7 +55,7 @@ section {
|
|||||||
p.stats {
|
p.stats {
|
||||||
padding-top: 3rem;
|
padding-top: 3rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: lightblue;
|
color: white;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
span.number {
|
span.number {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -70,14 +71,14 @@ section {
|
|||||||
.actions {
|
.actions {
|
||||||
padding-top: 3rem;
|
padding-top: 3rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: lightblue;
|
color: $primaryColor;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@include media($mobile) {
|
@include media($mobile) {
|
||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: lightblue;
|
color: $primaryColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +88,7 @@ button, input[type=submit] {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
border: 1px solid rgba(22, 21, 40, 1);
|
border: 1px solid rgba(22, 21, 40, 1);
|
||||||
background-color: rgba(22, 21, 40, 0.6);
|
background-color: rgba(22, 21, 40, 0.6);
|
||||||
color: lightblue;
|
color: $primaryColor;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ section#add-contributor, section#add-proposal {
|
|||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
a {
|
a {
|
||||||
color: lightblue;
|
color: $primaryColor;
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,56 +15,42 @@ table.contributor-list {
|
|||||||
|
|
||||||
&.metadata {
|
&.metadata {
|
||||||
height: 0;
|
height: 0;
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 0 1.2rem;
|
padding: 0 1.2rem;
|
||||||
transition: all 0.2s ease-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: lightblue;
|
color: $primaryColor;
|
||||||
&:hover, &:active {
|
&:hover, &:active {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dl {
|
ul {
|
||||||
|
list-style: none;
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
|
||||||
font-size: 1rem;
|
|
||||||
dt, dd {
|
|
||||||
display: inline-block;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 0;
|
height: 0;
|
||||||
line-height: 1.6rem;
|
|
||||||
transition: all 0.2s ease-out;
|
transition: all 0.2s ease-out;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline;
|
||||||
|
&+li {
|
||||||
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
dt {
|
|
||||||
clear: both;
|
|
||||||
float: left;
|
|
||||||
width: 30%;
|
|
||||||
}
|
|
||||||
dd {
|
|
||||||
float: right;
|
|
||||||
width: 70%;
|
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.visible {
|
&.visible {
|
||||||
td {
|
height: auto;
|
||||||
padding: 1rem 1.2rem;
|
ul {
|
||||||
}
|
|
||||||
|
|
||||||
dl {
|
|
||||||
dt, dd {
|
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 0 1.2rem;
|
padding: 0 1.2rem;
|
||||||
line-height: 4.2rem;
|
line-height: 4.2rem;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
margin-top: 12rem;
|
margin-top: 12rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
color: lightblue;
|
color: $primaryColor;
|
||||||
|
|
||||||
@include media($mobile) {
|
@include media($mobile) {
|
||||||
margin-top: 6rem;
|
margin-top: 6rem;
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
#path-comet {
|
#path-comet {
|
||||||
fill: lightblue;
|
fill: $primaryColor;
|
||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
|
|
||||||
animation-name: pulse;
|
animation-name: pulse;
|
||||||
|
|||||||
@@ -14,9 +14,14 @@ ul.proposal-list {
|
|||||||
border-top: 1px solid rgba(255,255,255,0.2);
|
border-top: 1px solid rgba(255,255,255,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.id {
|
.category {
|
||||||
color: lightblue;
|
color: $blue;
|
||||||
padding-right: 0.2rem;
|
padding-right: 0.2rem;
|
||||||
|
&.community { color: $red; }
|
||||||
|
&.dev { color: $pink; }
|
||||||
|
&.design { color: $yellow; }
|
||||||
|
&.docs { color: $green; }
|
||||||
|
&.ops { color: $purple; }
|
||||||
}
|
}
|
||||||
.amount {
|
.amount {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -31,8 +36,8 @@ ul.proposal-list {
|
|||||||
}
|
}
|
||||||
.votes {
|
.votes {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: lightblue;
|
color: $primaryColor;
|
||||||
padding-left: 1rem;
|
padding-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
|||||||
@@ -54,8 +54,8 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{add-contributor kredits=kredits
|
{{add-contributor contributors=model.contributors
|
||||||
contributors=model.contributors
|
newContributor=model.newContributor
|
||||||
contractInteractionEnabled=contractInteractionEnabled}}
|
contractInteractionEnabled=contractInteractionEnabled}}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ module.exports = function(environment) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
browserify: {
|
browserify: {
|
||||||
|
tests: true,
|
||||||
transform: [
|
transform: [
|
||||||
["babelify", {
|
["babelify", {
|
||||||
presets: ["es2015"],
|
presets: ["es2015"],
|
||||||
@@ -38,7 +39,7 @@ module.exports = function(environment) {
|
|||||||
ipfs: {
|
ipfs: {
|
||||||
host: 'ipfs.kosmos.org',
|
host: 'ipfs.kosmos.org',
|
||||||
port: '5444',
|
port: '5444',
|
||||||
protocol: 'http'
|
protocol: 'https'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Contribution dashboard of the Kosmos project",
|
"description": "Contribution dashboard of the Kosmos project",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Kosmos Community",
|
"author": "Kosmos Contributors <mail@kosmos.org>",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
"Garret Alfert <alfert@wevelop.de>",
|
"Garret Alfert <alfert@wevelop.de>",
|
||||||
"Sebastian Kippe <sebastian@kip.pe>",
|
"Sebastian Kippe <sebastian@kip.pe>",
|
||||||
@@ -55,8 +55,11 @@
|
|||||||
"ember-resolver": "^2.0.3",
|
"ember-resolver": "^2.0.3",
|
||||||
"ember-truth-helpers": "1.3.0",
|
"ember-truth-helpers": "1.3.0",
|
||||||
"ipfs-api": "^12.1.7",
|
"ipfs-api": "^12.1.7",
|
||||||
|
"kosmos-schemas": "~1.1.1",
|
||||||
"kredits-contracts": "^2.4.0",
|
"kredits-contracts": "^2.4.0",
|
||||||
"loader.js": "^4.0.10",
|
"loader.js": "^4.0.10",
|
||||||
|
"tv4": "^1.3.0",
|
||||||
|
"uuid": "^3.0.1",
|
||||||
"web3": "^0.18.2"
|
"web3": "^0.18.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
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:contributor', 'Unit | Model | contributor');
|
moduleFor('model:contributor', 'Unit | Model | contributor');
|
||||||
|
|
||||||
test('avatarURL returns correct URL', function(assert) {
|
test('#avatarURL() returns correct URL', function(assert) {
|
||||||
let model = this.subject();
|
let model = this.subject();
|
||||||
model.set('github_uid', '318');
|
model.set('github_uid', '318');
|
||||||
|
|
||||||
assert.equal(model.get('avatarURL'), 'https://avatars2.githubusercontent.com/u/318?v=3&s=128');
|
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 { 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('it exists', function(assert) {
|
test('#toJSON() requires a recipient profile IPFS hash to be set', function(assert) {
|
||||||
let model = this.subject();
|
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.