Skip to content

Commit

Permalink
Merged in feature/EF2E-1-AddedCreateLearnerButtonForCoach (pull request
Browse files Browse the repository at this point in the history
#5)

Feature/EF2E-1 AddedCreateLearnerFunctionality

Approved-by: Aleksandar Hummel <[email protected]>
Approved-by: Mayank Shah <[email protected]>
  • Loading branch information
Andrei Popescu committed Mar 22, 2019
2 parents 6372cba + 941bee4 commit b3f07ff
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 42 deletions.
3 changes: 3 additions & 0 deletions kolibri/core/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
from .permissions.general import IsFromSameFacility
from .permissions.general import IsOwn
from .permissions.general import IsSelf
from .permissions.general import AllowCoach
from kolibri.core.auth.constants.morango_scope_definitions import FULL_FACILITY
from kolibri.core.auth.constants.morango_scope_definitions import SINGLE_USER
from kolibri.core.errors import KolibriValidationError
Expand Down Expand Up @@ -522,6 +523,7 @@ class FacilityUser(KolibriAbstractBaseUser, AbstractFacilityDataModel):
permissions = (
IsSelf() | # FacilityUser can be read and written by itself
IsAdminForOwnFacility() | # FacilityUser can be read and written by a facility admin
AllowCoach() |
RoleBasedPermissions( # FacilityUser can be read by admin or coach, and updated by admin, but not created/deleted by non-facility admin
target_field=".",
can_be_created_by=(), # we can't check creation permissions by role, as user doesn't exist yet
Expand Down Expand Up @@ -903,6 +905,7 @@ class Membership(AbstractFacilityDataModel):
morango_model_name = "membership"

permissions = (
AllowCoach() |
IsOwn(read_only=True) | # users can read their own Memberships
RoleBasedPermissions( # Memberships can be read and written by admins, and read by coaches, for the member user
target_field="user",
Expand Down
49 changes: 49 additions & 0 deletions kolibri/core/auth/permissions/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,52 @@ def readable_by_user_filter(self, user, queryset):
return queryset.filter(dataset=user.dataset)
else:
return queryset.none()


class AllowCoach(BasePermissions):

def __init__(self, field_name=".", read_only=False):
self.read_only = read_only

def _user_is_coach(self, user, obj=None):

# Check if the current user is a coach

from ..models import Classroom

if obj:
if not hasattr(obj, "dataset") or not user.dataset == obj.dataset:
return False

classrooms = Classroom.objects.filter(dataset=user.dataset)

is_coach = 0

for classroom in classrooms:
if user.has_role_for_collection(role_kinds.COACH, classroom):
is_coach += 1

return is_coach > 0





def user_can_create_object(self, user, obj):
return (not self.read_only) and self._user_is_coach(user, obj)

def user_can_read_object(self, user, obj):
return self._user_is_coach(user, obj)

def user_can_update_object(self, user, obj):
return (not self.read_only) and self._user_is_coach(user, obj)

def user_can_delete_object(self, user, obj):
return (not self.read_only) and self._user_is_coach(user, obj)

def readable_by_user_filter(self, user, queryset):
if self._user_is_coach(user):
return queryset.filter(dataset=user.dataset)
else:
return queryset.none()

2 changes: 2 additions & 0 deletions kolibri/plugins/coach/assets/src/modules/pluginModule.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import userManagement from '../../../../facility_management/assets/src/modules/userManagement';
import classAssignMembers from '../../../../facility_management/assets/src/modules/classAssignMembers';
import getters from './coreCoach/getters';
import * as actions from './coreCoach/actions';
import examCreation from './examCreation';
Expand Down Expand Up @@ -56,6 +57,7 @@ export default {
lessonSummary,
lessonsRoot,
userManagement,
classAssignMembers,
reports,
},
};
6 changes: 3 additions & 3 deletions kolibri/plugins/coach/assets/src/views/ClassListPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
<td class="core-table-main-col">
<KRouterLink
:text="classroom.name"
:to="learnerPageLink(classroom.id)"
:to="learnerPageLink(classroom.id, classroom.name)"
/>
</td>
<td data-test="coach-names">
Expand Down Expand Up @@ -84,10 +84,10 @@
import { PageNames } from '../constants';
import { filterAndSortUsers } from '../../../../facility_management/assets/src/userSearchUtils';
function learnerPageLink(classId) {
function learnerPageLink(classId, className) {
return {
name: PageNames.LEARNER_LIST,
params: { classId },
params: { classId, className },
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>

<KModal
:title="$tr('createNewUserHeader')"
:title="$tr('createNewUserHeader',{thisClassName})"
:submitText="$tr('saveUserButtonLabel')"
:cancelText="$tr('cancel')"
:submitDisabled="submitting"
Expand Down Expand Up @@ -48,21 +48,6 @@
:invalidText="confirmedPasswordIsInvalidText"
@blur="confirmedPasswordBlurred = true"
/>

<fieldset v-if="coachIsSelected" class="coach-selector">
<KRadioButton
v-model="classCoach"
:label="$tr('classCoachLabel')"
:description="$tr('classCoachDescription')"
:value="true"
/>
<KRadioButton
v-model="classCoach"
:label="$tr('facilityCoachLabel')"
:description="$tr('facilityCoachDescription')"
:value="false"
/>
</fieldset>
</section>
</KModal>

Expand All @@ -73,16 +58,20 @@
import { mapActions, mapState, mapGetters } from 'vuex';
import { UserKinds, ERROR_CONSTANTS } from 'kolibri.coreVue.vuex.constants';
import { FacilityUsernameResource } from 'kolibri.resources';
import { validateUsername } from 'kolibri.utils.validators';
import CatchErrors from 'kolibri.utils.CatchErrors';
import KRadioButton from 'kolibri.coreVue.components.KRadioButton';
import KModal from 'kolibri.coreVue.components.KModal';
import KTextbox from 'kolibri.coreVue.components.KTextbox';
import {
filterAndSortUsers,
userMatchesFilter,
} from '../../../../../device_management/assets/src/userSearchUtils';
export default {
name: 'CoachUserCreateModal',
$trs: {
createNewUserHeader: 'Create new learner',
createNewUserHeader: 'Create new learner in {thisClassName}',
cancel: 'Cancel',
name: 'Full name',
username: 'Username',
Expand All @@ -91,13 +80,6 @@
userType: 'User type',
saveUserButtonLabel: 'Save',
learner: 'Learner',
coach: 'Coach',
admin: 'Admin',
coachSelectorHeader: 'Coach type',
classCoachLabel: 'Class coach',
classCoachDescription: "Can only instruct classes that they're assigned to",
facilityCoachLabel: 'Facility coach',
facilityCoachDescription: 'Can instruct all classes in your facility',
usernameAlreadyExists: 'Username already exists',
usernameNotAlphaNumUnderscore: 'Username can only contain letters, numbers, and underscores',
pwMismatchError: 'Passwords do not match',
Expand All @@ -106,12 +88,23 @@
required: 'This field is required',
},
components: {
KRadioButton,
KModal,
KTextbox,
},
props: {
classId: {
type: String,
required: true,
},
className: {
type: String,
required: true,
},
},
data() {
return {
thisClassName: this.className,
thisClassId: this.classId,
fullName: '',
username: '',
password: '',
Expand All @@ -128,24 +121,17 @@
passwordBlurred: false,
confirmedPasswordBlurred: false,
formSubmitted: false,
newUser: [],
usernames: [],
};
},
computed: {
...mapGetters(['currentFacilityId']),
...mapState('userManagement', ['facilityUsers']),
newUserRole() {
if (this.coachIsSelected) {
if (this.classCoach) {
return UserKinds.ASSIGNABLE_COACH;
}
return UserKinds.COACH;
}
// Admin or Learner
return this.kind.value;
},
coachIsSelected() {
return this.kind.value === UserKinds.COACH;
},
nameIsInvalidText() {
if (this.nameBlurred || this.formSubmitted) {
if (this.fullName === '') {
Expand All @@ -158,8 +144,8 @@
return Boolean(this.nameIsInvalidText);
},
usernameAlreadyExists() {
return this.facilityUsers.find(
({ username }) => username.toLowerCase() === this.username.toLowerCase()
return this.usernames.find(
username => username.toLowerCase() === this.username.toLowerCase()
);
},
usernameIsInvalidText() {
Expand Down Expand Up @@ -213,9 +199,13 @@
);
},
},
beforeMount() {
this.setSuggestions();
},
methods: {
...mapActions('userManagement', ['createUser', 'displayModal']),
...mapActions(['handleApiError']),
...mapActions('classAssignMembers', ['enrollLearnersInClass']),
createNewUser() {
this.usernameAlreadyExistsOnServer = false;
this.formSubmitted = true;
Expand All @@ -231,7 +221,9 @@
password: this.password,
}).then(
() => {
this.enrollLearnersInClass({ classId: this.thisClassId, users: this.getUsers() });
this.close();
location.reload();
},
error => {
const usernameAlreadyExistsError = CatchErrors(error, [
Expand All @@ -249,6 +241,26 @@
this.focusOnInvalidField();
}
},
setSuggestions() {
FacilityUsernameResource.fetchCollection({
getParams: {
facility: this.currentFacilityId,
},
})
.then(users => {
this.usernames = users.map(user => user.username);
})
.catch(() => {
this.usernames = [];
});
},
getUsers() {
this.newUser.push(
filterAndSortUsers(this.facilityUsers, user => userMatchesFilter(user, this.username))[0]
.id
);
return this.newUser;
},
focusOnInvalidField() {
if (this.nameIsInvalid) {
this.$refs.name.focus();
Expand Down
16 changes: 14 additions & 2 deletions kolibri/plugins/coach/assets/src/views/reports/LearnerListPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
</KGridItem>
<KGridItem sizes="100, 50, 50" percentage align="right">
<KButton
v-if="className"
:text="$tr('newUserButtonLabel')"
:primary="true"
@click="displayModal(Modals.COACH_CREATE_USER)"
Expand Down Expand Up @@ -84,7 +85,11 @@

<p v-if="!standardDataTable.length">{{ $tr('noLearners') }}</p>

<CoachUserCreateModal v-if="modalShown===Modals.COACH_CREATE_USER" />
<CoachUserCreateModal
v-if="modalShown===Modals.COACH_CREATE_USER"
:classId="thisClassId"
:className="thisClassName"
/>

</div>

Expand Down Expand Up @@ -117,6 +122,7 @@
metaInfo() {
return {
title: this.documentTitle,
className: this.className,
};
},
components: {
Expand Down Expand Up @@ -154,7 +160,7 @@
documentTitleForLearners: 'Learners',
},
computed: {
...mapState(['classId', 'pageName', 'reportRefreshInterval']),
...mapState(['classId', 'className', 'pageName', 'reportRefreshInterval']),
...mapGetters('reports', ['standardDataTable', 'exerciseCount', 'contentCount']),
...mapState('reports', ['channelId', 'contentScopeSummary', 'contentScopeId', 'userScope']),
...mapGetters(['currentUserId', 'isSuperuser', 'isCoach']),
Expand All @@ -173,6 +179,12 @@
isExercisePage() {
return this.contentScopeSummary.kind === ContentNodeKinds.EXERCISE;
},
thisClassName() {
return this.className;
},
thisClassId() {
return this.classId;
},
isRootLearnerPage() {
return this.pageName === PageNames.LEARNER_LIST;
},
Expand Down

0 comments on commit b3f07ff

Please sign in to comment.