Showing microposts
Although we don’t yet have a way to create microposts through the web - that comes in “Manipulating microposts Section” - this won’t stop us from displaying them (and testing that display).
Rendering microposts
Our plan is to display the microposts for each user on their respective profile page, together with a running count of how many microposts they’ve made. As we’ll see, many of the ideas are similar to our work in “Showing all users Section” on showing all users.
public/partials/microposts/_micropost.html
<a href ui-sref="user_detail({id: micropost.user.id})" ui-sref-opts="{reload: true}">
<img class="gravatar" gravatar_for="{{ micropost.user.email }}" alt="{{ micropost.user.name }}" options-size="50" />
</a>
<span class="user">
<a href ui-sref="user_detail({id: micropost.user.id})" ui-sref-opts="{reload: true}">
{{ micropost.user.name }}
</a>
</span>
<span class="content">{{ micropost.content }}</span>
<span time-ago class="timestamp">
Posted {{ time_ago_in_words(micropost.createdAt) }}.
</span>
This uses the timeAgo
module, whose meaning is probably clear and whose effect we will see in the next section.
~/sample_app $ npm install time-ago --save
public/index.html
...
<script src="../node_modules/pluralize/pluralize.js"></script>
<script src="../node_modules/time-ago/timeago.js"></script>
...
<script src="directives/message.js"></script>
<script src="directives/time.js"></script>
...
public/app.js
'use strict';
var sampleApp = angular.module('sampleApp', [
'timeDirective',
...
]);
...
public/directives/time.js
var timeDirective = angular.module('timeDirective', []);
timeDirective.directive('timeAgo', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
scope.time_ago_in_words = function(input) {
return timeago().ago(input);
};
}
};
});
Adding an microposts instance variable to the user show
app/controllers/users_controller.js
var sessionHelper = require('../helpers/sessions_helper.js');
function UsersController() {
...
this.show = function(req, res, next) {
var offset = (req.query.page - 1) * req.query.limit;
var user = ModelSync( User.findById(req.params.id) );
var microposts = ModelSync( Micropost.findAndCountAll({
where: { user_id: user.id },
include: [ { model: User } ],
order: 'micropost.createdAt DESC',
offset: offset,
limit: req.query.limit
}) );
res.end(JSON.stringify({
id: user.id,
email: user.email,
name: user.name,
microposts: microposts
}));
};
...
}
module.exports = UsersController;
Adding microposts to the user show page.
public/app.js
...
sampleApp.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/home');
$stateProvider
...
.state('user_detail', {
url: '/users/:id?page&limit',
templateUrl: 'partials/users/show.html',
resolve: {
current_user: current_user,
user: ['$q', '$stateParams', 'User', function($q, $stateParams, User){
$stateParams.page = $stateParams.page ? $stateParams.page : 1;
$stateParams.limit = $stateParams.limit ? $stateParams.limit : 30;
var deferred = $q.defer();
User.get({id: $stateParams.id, page: $stateParams.page, limit: $stateParams.limit}, function(user) {
deferred.resolve(user);
}, function(error) {
deferred.reject();
});
return deferred.promise;
}]
},
controller: 'UsersDetailCtrl'
})
...
}]);
...
public/partials/users/show.html
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<h1>
<img gravatar_for="{{ user.email }}" alt="{{ user.name }}" />
{{ user.name }}
</h1>
</section>
</aside>
<div class="col-md-8" ng-if="user.microposts.count">
<h3>Microposts ({{ user.microposts.count }})</h3>
<ol class="microposts">
<li ng-repeat="micropost in user.microposts.rows" id="micropost-" ng-include="'partials/microposts/_micropost.html'"></li>
</ol>
<uib-pagination total-items="totalItems" ng-model="currentPage" ng-change="pageChanged()" max-size="5" class="pagination-sm" boundary-link-numbers="true" rotate="false" items-per-page="itemsPerPage"></uib-pagination>
</div>
</div>
public/controllers/users_controller.js
...
usersController.controller(
'UsersDetailCtrl',
['$scope', '$rootScope', 'user', '$state', '$stateParams', function ($scope, $rootScope, user, $state, $stateParams) {
$rootScope.provide_title = user.name;
$scope.user = user;
$scope.totalItems = user.microposts.count;
}]
);
...
Sample microposts
Adding sample microposts for all the users actually takes a rather long time, so first we’ll select just the first six users
db/seeds.js
require('trainjs').initServer();
var faker = require('faker');
var time = 1;
function createUser() {
var name = faker.name.findName();
var email = 'example-'+time+'@railstutorial.org';
var password = 'password';
User.create({
name: name,
email: email,
password: password,
password_confirmation: password,
activated: true,
activated_at: new Date().getTime()
}).then(function(user_example) {
if (time < 98) {
if (time <= 4) {
createMicroposts(user_example.id);
}
time++;
createUser();
}
});
}
function createMicroposts(user_id) {
for (var i = 0; i < 50; i++) {
var content = faker.lorem.sentence();
Micropost.create({
content: content,
user_id: user_id
}).then(function() {
});
}
}
User.create({
name: "Example User",
email: "[email protected]",
password: "123456",
password_confirmation: "123456",
admin: true,
activated: true,
activated_at: new Date().getTime()
}).then(function(user1) {
createMicroposts(user1.id);
User.create({
name: "Example User",
email: "[email protected]",
password: "password",
password_confirmation: "password",
activated: true,
activated_at: new Date().getTime()
}).then(function(user2) {
createMicroposts(user2.id);
createUser();
});
});
At this point, we can reseed the development database as usual
~/sample_app $ rm -f db/development.sqlite3
~/sample_app $ sequelize db:migrate
~/sample_app $ node db/seeds.js
The CSS for microposts
public/assets/stylesheets/custom.css
/* microposts */
.microposts {
list-style: none;
padding: 0;
}
.microposts li {
padding: 10px 0;
border-top: 1px solid #e8e8e8;
}
.microposts .user {
margin-top: 5em;
padding-top: 0;
}
.microposts .content {
display: block;
margin-left: 60px;
}
.microposts .content img {
display: block;
padding: 5px 0;
}
.microposts .timestamp {
color: #999;
display: block;
margin-left: 60px;
}
.microposts .gravatar {
float: left;
margin-right: 10px;
margin-top: 5px;
}
aside textarea {
height: 100px;
margin-bottom: 5px;
}
span.picture {
margin-top: 10px;
}
span.picture input {
border: 0;
}
Profile micropost tests
Because newly activated users get redirected to their profile pages, we already have a test that the profile page renders correctly. In this section, we’ll write a short integration test for some of the other elements on the profile page, including the work from this section.
public/test/e2e_test/integration/users_profile_test.js
describe('UsersProfileTest', function() {
it('profile display', function(done) {
browser.get('http://localhost:1337/#/login');
var profile = function() {
browser.get('http://localhost:1337/#/login');
element(by.css('[name="email"]')).clear('');
element(by.css('[name="password"]')).clear('');
element(by.css('[name="email"]')).sendKeys('[email protected]');
element(by.css('[name="password"]')).sendKeys('password');
element(by.css('[name="commit"]')).click();
expect(browser.getTitle()).toEqual('Example User | Node On Train Tutorial Sample App');
expect( element(by.css('.user_info > h1')).getText() ).toEqual('Example User');
expect( element(by.css('.user_info > h1 > img[gravatar_for]')).isDisplayed() ).toBeTruthy();
expect( element(by.css('[ng-if="user.microposts.count"] > h3')).getText() ).toContain('50');
expect( element.all(by.css('.pagination-page')).count() ).toBeGreaterThan(0);
done();
};
element.all(by.css('[ui-sref="login"]')).isDisplayed().then(function(result) {
if ( result.length > 0 ) {
profile();
} else {
element(by.css('.dropdown')).click();
element(by.css('[ui-sref="logout"]')).click();
profile();
}
});
})
})
Because the application code was working, the test suite should be successful
~/sample_app $ protractor protractor.conf.js
29 specs, 0 failures