Skip to content
This repository has been archived by the owner on Apr 16, 2024. It is now read-only.

Commit

Permalink
Merge branch 'develop' into feature/#582-unit-visibility-toggle-2
Browse files Browse the repository at this point in the history
  • Loading branch information
dboschm authored Apr 27, 2018
2 parents f842977 + b9a0769 commit b7e9571
Show file tree
Hide file tree
Showing 36 changed files with 631 additions and 287 deletions.
2 changes: 1 addition & 1 deletion api/fixtures/courses/Access-key-test.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"description": "Documents the course fixture.",
"units": [
{
"name": "What is course fixture for?",
"name": "What is the purpose of this course fixture?",
"description": "",
"markdown": "To test the 'accesskey' enrollType.",
"__t": "free-text"
Expand Down
393 changes: 239 additions & 154 deletions api/src/controllers/CourseController.ts

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions api/src/models/Course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ courseSchema.methods.checkPrivileges = function (user: IUser) {
const userIsStudent: boolean = user.role === 'student';
// NOTE: The 'tutor' role exists and has fixtures, but currently appears to be unimplemented.
// const userIsTutor: boolean = user.role === 'tutor';

const courseAdminId = extractMongoId(this.courseAdmin);

const userIsCourseAdmin: boolean = user._id === courseAdminId;
Expand All @@ -209,9 +210,9 @@ courseSchema.methods.checkPrivileges = function (user: IUser) {
const userCanViewCourse: boolean = (this.active && userIsCourseStudent) || userCanEditCourse;

return {userIsAdmin, userIsTeacher, userIsStudent,
courseAdminId,
userIsCourseAdmin, userIsCourseTeacher, userIsCourseStudent, userIsCourseMember,
userCanEditCourse, userCanViewCourse};
courseAdminId,
userIsCourseAdmin, userIsCourseTeacher, userIsCourseStudent, userIsCourseMember,
userCanEditCourse, userCanViewCourse};
};

courseSchema.methods.forDashboard = function (user: IUser): ICourseDashboard {
Expand Down Expand Up @@ -250,7 +251,7 @@ courseSchema.methods.populateLecturesFor = function (user: IUser) {
path: 'units',
virtuals: true,
match: {$or: [{visible: undefined}, {visible: true}, {visible: !isTeacherOrAdmin}]},
populate: {
populate: {
path: 'progressData',
match: {user: {$eq: user._id}}
}
Expand Down
10 changes: 10 additions & 0 deletions api/src/models/Lecture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import * as mongoose from 'mongoose';
import {ILecture} from '../../../shared/models/ILecture';
import {IUnitModel, Unit} from './units/Unit';
import {IUnit} from '../../../shared/models/units/IUnit';
import {IUser} from '../../../shared/models/IUser';
import {InternalServerError} from 'routing-controllers';
import {Course} from './Course';
import * as winston from 'winston';

interface ILectureModel extends ILecture, mongoose.Document {
exportJSON: () => Promise<ILecture>;
processUnitsFor: (user: IUser) => Promise<this>;
}

const lectureSchema = new mongoose.Schema({
Expand Down Expand Up @@ -73,6 +75,14 @@ lectureSchema.methods.exportJSON = async function() {
return obj;
};

lectureSchema.methods.processUnitsFor = async function (user: IUser) {
this.units = await Promise.all(this.units.map(async (unit: IUnitModel) => {
unit = await unit.populateUnit();
return unit.secureData(user);
}));
return this;
};

lectureSchema.statics.importJSON = async function(lecture: ILecture, courseId: string) {
// importTest lectures
const units: Array<IUnit> = lecture.units;
Expand Down
12 changes: 10 additions & 2 deletions api/src/models/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,16 @@ const userSchema = new mongoose.Schema({
validate: new RegExp(errorCodes.errorCodes.password.regex.regex)
},
profile: {
firstName: {type: String, index: true},
lastName: {type: String, index: true},
firstName: {
type: String,
index: true,
maxlength: 64
},
lastName: {
type: String,
index: true,
maxlength: 64
},
picture: {
path: {type: String},
name: {type: String},
Expand Down
42 changes: 42 additions & 0 deletions api/src/utilities/ExtractMongoId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Tries to extract the id from a mongoose object.
* This could be a node-mongodb-native ObjectID (i.e. mongoose.Types.ObjectId), via toHexString,
* or anything that contains an 'id' property, which is directly returned.
*
* @param from A mongoose object.
* @param fallback Return this if no id is found.
*/
function extractMongoIdImpl(from: any, fallback?: any) {
if (from instanceof Object) {
if (from._bsontype === 'ObjectID') {
return from.toString();
} else if ('id' in from) {
return from.id;
}
}
return fallback;
}

/**
* Tries to extract the id from a mongoose object or array of such.
* This could be a node-mongodb-native ObjectID (i.e. mongoose.Types.ObjectId), via toHexString,
* or anything that contains an 'id' property, which is directly returned.
* For arrays, anything === undefined won't be pushed.
*
* @param from A mongoose object or array of such.
* @param fallback Return this if no id is found.
*/
export function extractMongoId(from: any | any[], fallback?: any) {
if (Array.isArray(from)) {
const results: any[] = [];
for (const value of from) {
const result = extractMongoIdImpl(value, fallback);
if (result !== undefined) {
results.push(result);
}
}
return results;
} else {
return extractMongoIdImpl(from, fallback);
}
}
80 changes: 66 additions & 14 deletions api/test/integration/course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import {Server} from '../../src/server';
import {FixtureLoader} from '../../fixtures/FixtureLoader';
import {JwtUtils} from '../../src/security/JwtUtils';
import {User} from '../../src/models/User';
import {Course} from '../../src/models/Course';
import {Course, ICourseModel} from '../../src/models/Course';
import {ICourse} from '../../../shared/models/ICourse';
import {ICourseView} from '../../../shared/models/ICourseView';
import {IUser} from '../../../shared/models/IUser';
import {FixtureUtils} from '../../fixtures/FixtureUtils';
import chaiHttp = require('chai-http');

Expand Down Expand Up @@ -106,24 +109,79 @@ describe('Course', () => {


describe(`GET ${BASE_URL} :id`, () => {
it('should get course with given id', async () => {
const teacher = await FixtureUtils.getRandomTeacher();
async function prepareTestCourse() {
const teachers = await FixtureUtils.getRandomTeachers(2, 2);
const teacher = teachers[0];
const unauthorizedTeacher = teachers[1];
const student = await FixtureUtils.getRandomStudent();
const testData = new Course({
name: 'Test Course',
description: 'Test description',
active: true,
courseAdmin: teacher._id
courseAdmin: teacher._id,
enrollType: 'accesskey',
accessKey: 'accessKey1234',
students: [student._id]
});
const savedCourse = await testData.save();
return {teacher, unauthorizedTeacher, student, testData, savedCourse};
}

async function testUnauthorizedGetCourseEdit(savedCourse: ICourseModel, user: IUser) {
const res = await chai.request(app)
.get(`${BASE_URL}/${savedCourse._id}/edit`)
.set('Authorization', `JWT ${JwtUtils.generateToken(user)}`);

res.should.not.have.status(200);
return res;
}

it('should get view info for course with given id', async () => {
const {student, testData, savedCourse} = await prepareTestCourse();

const res = await chai.request(app)
.get(`${BASE_URL}/${savedCourse._id}`)
.set('Authorization', `JWT ${JwtUtils.generateToken(student)}`);

res.should.have.status(200);

const body: ICourseView = res.body;
body.name.should.be.equal(testData.name);
body.description.should.be.equal(testData.description);

should.equal(res.body.accessKey, undefined);
});

it('should get edit info for course with given id', async () => {
const {teacher, testData, savedCourse} = await prepareTestCourse();

const res = await chai.request(app)
.get(`${BASE_URL}/${savedCourse._id}/edit`)
.set('Authorization', `JWT ${JwtUtils.generateToken(teacher)}`);

res.should.have.status(200);
res.body.name.should.be.equal(testData.name);
res.body.description.should.be.equal(testData.description);
res.body.active.should.be.equal(testData.active);

const body: ICourse = res.body;
body.name.should.be.equal(testData.name);
body.description.should.be.equal(testData.description);
body.active.should.be.equal(testData.active);
body.enrollType.should.be.equal(testData.enrollType);
body.accessKey.should.be.equal(testData.accessKey);
});

it('should not get edit info for course as student', async () => {
const {savedCourse, student} = await prepareTestCourse();
const res = await testUnauthorizedGetCourseEdit(savedCourse, student);
res.body.name.should.be.equal('AccessDeniedError');
res.body.should.have.property('message');
res.body.should.have.property('stack');
});

it('should not get edit info for course as unauthorized teacher', async () => {
const {savedCourse, unauthorizedTeacher} = await prepareTestCourse();
const res = await testUnauthorizedGetCourseEdit(savedCourse, unauthorizedTeacher);
res.body.name.should.be.oneOf(['NotFoundError', 'ForbiddenError']);
res.body.should.have.property('stack');
});

it('should not get course not a teacher of course', async () => {
Expand Down Expand Up @@ -176,7 +234,7 @@ describe('Course', () => {
res.body._id.should.be.eq(testDataUpdate.id);

res = await chai.request(app)
.get(`${BASE_URL}/${res.body._id}`)
.get(`${BASE_URL}/${res.body._id}/edit`)
.set('Authorization', `JWT ${JwtUtils.generateToken(teacher)}`);

res.should.have.status(200);
Expand Down Expand Up @@ -247,9 +305,3 @@ describe('Course', () => {
});
});
});






42 changes: 41 additions & 1 deletion api/test/unit/utilities.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import {expect} from 'chai';
import {IProperties} from '../../../shared/models/IProperties';

import Pick from '../../src/utilities/Pick';
import {IProperties} from '../../../shared/models/IProperties';

import {extractMongoId} from '../../src/utilities/ExtractMongoId';
import {Types as mongooseTypes} from 'mongoose';

describe('Testing utilities', () => {
describe('Pick', () => {
Expand All @@ -20,4 +24,40 @@ describe('Testing utilities', () => {
expect(result).to.eql({c: [], d: {}});
});
});

describe('ExtractMongoId', () => {
const idAsObjectId = mongooseTypes.ObjectId();
const idDirect = {id: idAsObjectId.toHexString()};
const idArray = [idAsObjectId, idDirect, {}, idAsObjectId, idDirect, 1, 'id', idDirect];
const idExpect = idDirect.id;
const fallback = 'fallback';

it('should extract an id from an ObjectID object', () => {
expect(extractMongoId(idAsObjectId)).to.eq(idExpect);
});

it('should extract an id from an object with "id" property', () => {
expect(extractMongoId(idDirect)).to.eq(idExpect);
});

it('should yield undefined for invalid input', () => {
expect(extractMongoId({})).to.eq(undefined);
expect(extractMongoId(1)).to.eq(undefined);
expect(extractMongoId('id')).to.eq(undefined);
});

it('should return a specified fallback for invalid input', () => {
expect(extractMongoId({}, fallback)).to.eq(fallback);
expect(extractMongoId(1, fallback)).to.eq(fallback);
expect(extractMongoId('id', fallback)).to.eq(fallback);
});

it('should only extract ids for valid objects in an array', () => {
expect(extractMongoId(idArray)).to.eql([idExpect, idExpect, idExpect, idExpect, idExpect]);
});

it('should extract ids for valid objects or return fallback values in an array', () => {
expect(extractMongoId(idArray, 'fallback')).to.eql([idExpect, idExpect, fallback, idExpect, idExpect, fallback, fallback, idExpect]);
});
});
});
2 changes: 1 addition & 1 deletion app/webFrontend/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<span *ngIf="isLoggedIn()">
<button mat-button routerLink="/profile">
<img class="avatar" src="{{userService.user.getUserImageURL(30)}}" width="30" height="30">
{{ this.userService.user.profile.firstName }}
<span class="firstname-wrapper">{{ this.userService.user.profile.firstName }}</span>
</button>
<app-notification *ngIf="isStudent()"></app-notification>
</span>
Expand Down
29 changes: 29 additions & 0 deletions app/webFrontend/src/app/app.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,32 @@
background: url('/assets/people-woman-coffee-meeting.jpg') no-repeat;
background-size: cover;
}

.firstname-wrapper {
@media screen and (max-width: $bp-width--md) {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

@media screen and (max-width: 1065px) {
max-width: 40rem;

@media screen and (max-width: 750px) {
max-width: 15rem;

@media screen and (max-width: 500px) {
max-width: 10rem;

@media screen and (max-width: 400px) {
max-width: 5rem;

@media screen and (max-width: 320px) {
max-width: 2.5rem;
}
}
}
}
}
}
}
12 changes: 10 additions & 2 deletions app/webFrontend/src/app/auth/register/register.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<div class="form-group" formGroupName="profile">
<mat-form-field>
<input matInput formControlName="firstName"
[placeholder]="'common.profile.firstName'|translate"/>
[placeholder]="'common.profile.firstName'|translate" maxlength="64"/>
<div *ngIf="registerForm.get('profile').get('firstName').touched">
<small *ngIf="registerForm.get('profile').get('firstName').hasError('required')"
class="text-danger">
Expand All @@ -31,11 +31,15 @@
class="text-danger" translate [translateParams]="{min:2}">
common.validation.minLength
</small>
<small *ngIf="registerForm.get('profile').get('firstName').hasError('maxlength')"
class="text-danger" translate [translateParams]="{max:64}">
common.validation.maxLength
</small>
</div>
</mat-form-field>
<mat-form-field>
<input matInput formControlName="lastName"
[placeholder]="'common.profile.lastName'|translate"/>
[placeholder]="'common.profile.lastName'|translate" maxlength="64"/>
<div *ngIf="registerForm.get('profile').get('lastName').touched">
<small *ngIf="registerForm.get('profile').get('lastName').hasError('required')"
class="text-danger">
Expand All @@ -45,6 +49,10 @@
class="text-danger" translate [translateParams]="{min:2}">
common.validation.minLength
</small>
<small *ngIf="registerForm.get('profile').get('lastName').hasError('maxlength')"
class="text-danger" translate [translateParams]="{max:64}">
common.validation.maxLength
</small>
</div>
</mat-form-field>
</div>
Expand Down
Loading

0 comments on commit b7e9571

Please sign in to comment.