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