diff --git a/src/controllers/IssueController.js b/src/controllers/IssueController.js
new file mode 100644
index 0000000..3ec85c0
--- /dev/null
+++ b/src/controllers/IssueController.js
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2018 TopCoder, Inc. All rights reserved.
+ */
+
+/**
+ * This controller exposes project endpoints.
+ *
+ * @author veshu
+ * @version 1.0
+ */
+const helper = require('../common/helper');
+const IssueService = require('../services/IssueService');
+
+/**
+ * search issues
+ * @param {Object} req the request
+ * @param {Object} res the response
+ * @returns {Object} the result
+ */
+async function search(req) {
+ return await IssueService.search(req.query, req.currentUser.handle);
+}
+
+module.exports = {
+ search,
+};
+
+helper.buildController(module.exports);
\ No newline at end of file
diff --git a/src/front/src/app/app.js b/src/front/src/app/app.js
index ce41c2b..0f6aeb4 100644
--- a/src/front/src/app/app.js
+++ b/src/front/src/app/app.js
@@ -68,6 +68,8 @@ angular.module('topcoderX', [
})
.state('app.main', {
url: '/main',
+ controller: 'MainController',
+ controllerAs: 'vm',
templateUrl: 'app/main/main.html',
data: { pageTitle: 'Dashboard' },
resolve: { auth: authenticate }
diff --git a/src/front/src/app/main/issue.service.js b/src/front/src/app/main/issue.service.js
new file mode 100644
index 0000000..af75acf
--- /dev/null
+++ b/src/front/src/app/main/issue.service.js
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2018 TopCoder, Inc. All rights reserved.
+ *
+ * This is a service to access the backend api for issue related operation.
+ */
+'use strict';
+
+angular.module('topcoderX')
+ .factory('IssueService', ['$http', 'Helper', function ($http, Helper) {
+ var baseUrl = Helper.baseUrl;
+ var service = {};
+
+ /**
+ * search for issues
+ */
+ service.search = function (label, sortBy, sortDir, pageNo, pageSize) {
+ return $http.get(baseUrl + '/api/v1/issues?label=' + label + '&sortBy=' + sortBy + '&sortDir=' + sortDir + '&page=' + pageNo + '&perPage=' + pageSize).then(function (response) {
+ return response;
+ });
+ };
+
+ return service;
+ }]);
diff --git a/src/front/src/app/main/main.controller.js b/src/front/src/app/main/main.controller.js
index afd4bf1..021a7c0 100644
--- a/src/front/src/app/main/main.controller.js
+++ b/src/front/src/app/main/main.controller.js
@@ -1,18 +1,141 @@
'use strict';
angular.module('topcoderX')
- .controller('MainController', ['$scope', '$rootScope', '$timeout', '$state', 'AuthService',
- function ($scope, $rootScope, $timeout, $state, AuthService) {
+ .controller('MainController', ['$scope', '$rootScope', 'Alert', '$state', 'AuthService', 'IssueService',
+ function ($scope, $rootScope, Alert, $state, AuthService, IssueService) {
+ $scope.isLoaded = false;
+ $scope.tableConfig = {
+ readyForReview: {
+ pageNumber: 1,
+ pageSize: 20,
+ isLoading: false,
+ items: [],
+ label: 'Ready for review',
+ sortBy: 'updatedAt',
+ sortDir: 'desc',
+ totalPages: 1,
+ initialized: false,
+ },
+ assigned: {
+ pageNumber: 1,
+ pageSize: 20,
+ isLoading: false,
+ items: [],
+ label: 'Assigned',
+ sortBy: 'updatedAt',
+ sortDir: 'desc',
+ totalPages: 1,
+ initialized: false,
+ },
+ openForPickup: {
+ pageNumber: 1,
+ pageSize: 20,
+ isLoading: false,
+ items: [],
+ label: 'Open for pickup',
+ sortBy: 'updatedAt',
+ sortDir: 'desc',
+ totalPages: 1,
+ initialized: false,
+ },
+ paid: {
+ pageNumber: 1,
+ pageSize: 20,
+ isLoading: false,
+ items: [],
+ label: 'Paid',
+ sortBy: 'updatedAt',
+ sortDir: 'desc',
+ totalPages: 1,
+ initialized: false,
+ },
+ };
$rootScope.currentUser = AuthService.getCurrentUser();
$scope.logout = function () {
AuthService.logout();
- $state.go('auth')
+ $state.go('auth');
};
// auth
$scope.authorized = function () {
return AuthService.isLoggedIn();
};
- }
- ]);
+
+ var _search = function (provider) {
+ var config = $scope.tableConfig[provider];
+ config.isLoading = true;
+ IssueService.search(config.label, config.sortBy, config.sortDir, config.pageNumber, config.pageSize)
+ .then(function (res) {
+ config.items = res.data.docs;
+ config.pages = res.data.pages;
+ config.isLoading = false;
+ config.initialized = true;
+ }).catch(function (err) {
+ config.isLoading = false;
+ config.initialized = true;
+ _handleError(err, 'An error occurred while getting the data for ' + provider + '.');
+ });
+ };
+
+ _search('readyForReview');
+
+ // handle errors
+ function _handleError(error, defaultMsg) {
+ var errMsg = error.data ? error.data.message : defaultMsg;
+ Alert.error(errMsg, $scope);
+ }
+
+ // change to a specific page
+ $scope.changePage = function (pageNumber, provider) {
+ if (pageNumber === 0 || pageNumber > $scope.tableConfig[provider].pages ||
+ (pageNumber === $scope.tableConfig[provider].pages &&
+ $scope.tableConfig[provider].pageNumber === pageNumber)) {
+ return false;
+ }
+ $scope.tableConfig[provider].pageNumber = pageNumber;
+ _search(provider);
+ };
+
+ $scope.tabChanged = function (provider) {
+ $scope.tableConfig[provider].sortBy = 'updatedAt';
+ $scope.tableConfig[provider].sortDir = 'desc';
+ $scope.tableConfig[provider].pageNumber = 1;
+ $scope.tableConfig[provider].initialized = false;
+ _search(provider);
+ };
+
+ // get the number array that shows the pagination bar
+ $scope.getPageArray = function (provider) {
+ var res = [];
+
+ var pageNo = $scope.tableConfig[provider].pageNumber;
+ var i = pageNo - 5;
+ for (i; i <= pageNo; i++) {
+ if (i > 0) {
+ res.push(i);
+ }
+ }
+ var j = pageNo + 1;
+ for (j; j <= $scope.tableConfig[provider].pages && j <= pageNo + 5; j++) {
+ res.push(j);
+ }
+ return res;
+ };
+
+ // sort by criteria
+ $scope.sort = function (criteria, provider) {
+ if (criteria === $scope.tableConfig[provider].sortBy) {
+ if ($scope.tableConfig[provider].sortDir === 'asc') {
+ $scope.tableConfig[provider].sortDir = 'desc';
+ } else {
+ $scope.tableConfig[provider].sortDir = 'asc';
+ }
+ } else {
+ $scope.tableConfig[provider].sortDir = 'asc';
+ }
+ $scope.tableConfig[provider].sortBy = criteria;
+ $scope.tableConfig[provider].pageNumber = 1;
+ _search(provider);
+ };
+ }]);
diff --git a/src/front/src/app/main/main.html b/src/front/src/app/main/main.html
index 24f4853..8878246 100644
--- a/src/front/src/app/main/main.html
+++ b/src/front/src/app/main/main.html
@@ -2,14 +2,280 @@
Dashboard
-
-
+
+
+
+
+
+
+
+
+
+ No tickets found
+
+
+
+
+
+
+
+
+ No tickets found
+
+
+
+
+
+
+
+
+ No tickets found
+
+
+
+
+
+
+
+
+ No tickets found
+
+
+
+
diff --git a/src/models/Issue.js b/src/models/Issue.js
new file mode 100644
index 0000000..4c8168b
--- /dev/null
+++ b/src/models/Issue.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2017 TopCoder, Inc. All rights reserved.
+ */
+
+/**
+ * Schema for Issue.
+ * @author TCSCODER
+ * @version 1.0
+ */
+const mongoose = require('mongoose');
+
+const schema = new mongoose.Schema({
+ // From the receiver service
+ number: {type: Number, required: true},
+ title: {type: String, required: true},
+ body: String,
+ prizes: [{type: Number, required: true}], // extracted from title
+ provider: {type: String, required: true}, // github or gitlab
+ repositoryId: {type: Number, required: true},
+ labels: [{type: String, required: true}],
+ assignee: {type: String, required: false},
+ updatedAt: {
+ type: Date,
+ default: Date.now,
+ },
+ // From topcoder api
+ challengeId: {type: Number, required: true, unique: true},
+ projectId: {type: mongoose.Schema.Types.ObjectId, ref: 'Project'},
+});
+
+// Issue number, provider, repositoryId must be unique
+schema.index({number: 1, provider: 1, repositoryId: 1}, {unique: true});
+schema.index({labels: 1});
+schema.index({projectId: 1});
+
+schema.pre('save', function preSave(next) {
+ this.updatedAt = Date.now(); // eslint-disable-line
+ return next();
+});
+module.exports = schema;
diff --git a/src/routes.js b/src/routes.js
index b8fcc02..5a1c2f4 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -95,6 +95,7 @@ module.exports = {
get: {
controller: 'TCUserController',
method: 'login',
+ allowNormalUser: true,
},
},
'/admin/tcuser': {
@@ -148,4 +149,10 @@ module.exports = {
allowNormalUser: true,
},
},
+ '/issues': {
+ get: {
+ controller: 'IssueController',
+ method: 'search',
+ },
+ },
};
diff --git a/src/services/GithubService.js b/src/services/GithubService.js
index 964962e..ad6605e 100644
--- a/src/services/GithubService.js
+++ b/src/services/GithubService.js
@@ -10,6 +10,7 @@
*/
const GitHub = require('github-api');
const Joi = require('joi');
+const _ = require('lodash');
const config = require('../config');
const constants = require('../common/constants');
const helper = require('../common/helper');
@@ -173,22 +174,27 @@ getTeamRegistrationUrl.schema = Joi.object().keys({
* @returns {Promise} the promise result
*/
async function addTeamMember(teamId, ownerUserToken, normalUserToken) {
+ let username;
+ let id;
try {
// get normal user name
const githubNormalUser = new GitHub({ token: normalUserToken });
const normalUser = await githubNormalUser.getUser().getProfile();
- const username = normalUser.data.login;
- const id = normalUser.data.id;
+ username = normalUser.data.login;
+ id = normalUser.data.id;
// add normal user to team
const github = new GitHub({ token: ownerUserToken });
const team = github.getTeam(teamId);
await team.addMembership(username);
- // return github username
- return { username, id };
} catch (err) {
- throw helper.convertGitHubError(err, 'Failed to add team member');
+ // if error is already exists discard
+ if (_.chain(err).get('body.errors').countBy({ code: 'already_exists' }).get('true').isUndefined().value()) {
+ throw helper.convertGitHubError(err, 'Failed to add team member');
+ }
}
+ // return github username
+ return {username, id};
}
addTeamMember.schema = Joi.object().keys({
diff --git a/src/services/GitlabService.js b/src/services/GitlabService.js
index 42488c4..e8635dd 100644
--- a/src/services/GitlabService.js
+++ b/src/services/GitlabService.js
@@ -11,6 +11,7 @@
const Joi = require('joi');
const superagent = require('superagent');
const superagentPromise = require('superagent-promise');
+const _ = require('lodash');
const config = require('../config');
const constants = require('../common/constants');
const helper = require('../common/helper');
@@ -167,13 +168,16 @@ getGroupRegistrationUrl.schema = Joi.object().keys({
* @returns {Promise} the promise result
*/
async function addGroupMember(groupId, ownerUserToken, normalUserToken) {
+ let username;
+ let userId;
try {
// get normal user id
const res = await request
.get(`${config.GITLAB_API_BASE_URL}/api/v4/user`)
.set('Authorization', `Bearer ${normalUserToken}`)
.end();
- const userId = res.body.id;
+ userId = res.body.id;
+ username = res.body.username;
if (!userId) {
throw new errors.UnauthorizedError('Can not get user id from the normal user access token.');
}
@@ -187,10 +191,13 @@ async function addGroupMember(groupId, ownerUserToken, normalUserToken) {
// return gitlab username
return { username: res.body.username, id: res.body.id };
} catch (err) {
- if (err instanceof errors.ApiError) {
- throw err;
+ if (_.get(JSON.parse(err.response.text), 'message') !== 'Member already exists') {
+ if (err instanceof errors.ApiError) {
+ throw err;
+ }
+ throw helper.convertGitLabError(err, 'Failed to add group member');
}
- throw helper.convertGitLabError(err, 'Failed to add group member');
+ return {username, id: userId};
}
}
diff --git a/src/services/IssueService.js b/src/services/IssueService.js
new file mode 100644
index 0000000..050a4eb
--- /dev/null
+++ b/src/services/IssueService.js
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2018 TopCoder, Inc. All rights reserved.
+ */
+
+/**
+ * This service will provide project operations.
+ *
+ * @author TCSCODER
+ * @version 1.0
+ */
+const Joi = require('joi');
+const _ = require('lodash');
+const helper = require('../common/helper');
+const models = require('../models');
+
+
+/**
+ * searches the issues
+ * @param {Object} criteria the search criteria
+ * @param {String} currentUserTopcoderHandle current user's topcoder handle
+ * @returns {Object} the search results
+ */
+async function search(criteria, currentUserTopcoderHandle) {
+ const query = {};
+ if (criteria.label) {
+ query.labels = { $in: [criteria.label] };
+ }
+
+ // select projects for current user
+ const projects = await models.Project.find({ username: currentUserTopcoderHandle, archived: false });
+ query.projectId = {
+ $in: projects.map((i) => i._id),
+ };
+
+ if (!criteria.sortBy) {
+ criteria.sortBy = 'updatedAt';
+ criteria.sortDir = 'desc';
+ }
+ const docs = await models.Issue.find(query)
+ .populate({ path: 'projectId', select: 'title repoUrl' });
+ const offset = (criteria.page - 1) * criteria.perPage;
+ const result = {
+ pages: Math.ceil(docs.length / criteria.perPage) || 1,
+ docs: _(docs).orderBy(criteria.sortBy, criteria.sortDir)
+ .slice(offset).take(criteria.perPage)
+ .value(),
+ };
+ return result;
+}
+
+search.schema = Joi.object().keys({
+ criteria: Joi.object().keys({
+ label: Joi.string().required(),
+ sortBy: Joi.string().valid('title', 'projectId.title', 'updatedAt', 'assignee').default('updatedAt'),
+ sortDir: Joi.string().valid('asc', 'desc').default('asc'),
+ page: Joi.number().integer().min(1).required(),
+ perPage: Joi.number().integer().min(1).required(),
+ }),
+ currentUserTopcoderHandle: Joi.string().required(),
+});
+
+module.exports = {
+ search,
+};
+
+helper.buildService(module.exports);