Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Organize students #162

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ db-reset:
# 'database=' here is a variable used in schema.sql (-v).
psql -v database=memcode -U postgres -f backend/db/schema.sql
db-migrate:
psql -v database=memcode -U postgres -f backend/db/migrations/14.sql
psql -v database=memcode -U postgres -f backend/db/migrations/15.sql

# dump and restore data
db-dump:
Expand Down
13 changes: 13 additions & 0 deletions backend/api/StudentGroupApi/create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import knex from '~/db/knex';
import auth from '~/middlewares/auth';

const create = auth(async (request, response) => {
const userId = request.currentUser.id;
const title = request.body['title'];

const createdGroup = (await knex('studentGroup').insert({ title, userId }).returning('*'))[0];

response.success(createdGroup);
});

export default create;
22 changes: 22 additions & 0 deletions backend/api/StudentGroupApi/getAll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import knex from '~/db/knex';
import auth from '~/middlewares/auth';

const getAll = auth(async (request, response) => {
const userId = request.currentUser.id;

const studentGroups = await knex('studentGroup')
.select('*')
.where({ userId });

const students = await knex('studentInGroup')
.select('*')
.join('user', { 'studentInGroup.userId': 'user.id' })
.whereIn('studentGroupId', studentGroups.map((group) => group.id));

response.success({
studentGroups,
students
});
});

export default getAll;
7 changes: 7 additions & 0 deletions backend/api/StudentGroupApi/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import create from './create';
import getAll from './getAll';

export default {
create,
getAll
};
2 changes: 2 additions & 0 deletions backend/api/urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import CourseUserIsLearningApi from '~/api/CourseUserIsLearningApi';
import ProblemUserIsLearningApi from '~/api/ProblemUserIsLearningApi';
import UserApi from '~/api/UserApi';
import ProblemApi from '~/api/ProblemApi';
import StudentGroupApi from '~/api/StudentGroupApi';

const getApiClass = (controllerName) => {
switch (controllerName) {
Expand All @@ -19,6 +20,7 @@ const getApiClass = (controllerName) => {
case 'ProblemUserIsLearningApi': return ProblemUserIsLearningApi;
case 'UserApi': return UserApi;
case 'PageApi': return PageApi;
case 'StudentGroupApi': return StudentGroupApi;
}
};

Expand Down
19 changes: 19 additions & 0 deletions backend/db/migrations/15.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
\c :database;

CREATE TABLE student_group (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES "user" (id) ON DELETE CASCADE NOT NULL,
title VARCHAR NOT NULL CHECK (char_length(title) >= 1),
created_at TIMESTAMP NOT NULL DEFAULT now()
);

CREATE TABLE student_in_group (
id SERIAL PRIMARY KEY,
student_group_id INTEGER REFERENCES "student_group" (id) ON DELETE CASCADE NOT NULL,
user_id INTEGER REFERENCES "user" (id) ON DELETE CASCADE NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now(),
unique (student_group_id, user_id)
);

-- When the teacher creates some group of students,
-- they will see these students grouped in every course they create.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import api from '~/api';

import TogglerAndModal from '~/components/TogglerAndModal';
import { TextInput } from '~/components/_standardForm';

import disableOnSpeRequest from '~/services/disableOnSpeRequest';

class CreateGroupModal extends React.Component {
static propTypes = {
toggler: PropTypes.object.isRequired,
uiCreateStudentGroup: PropTypes.func.isRequired,
}

state = {
formState: {
title: ''
},
formValidation: {},
speCreateGroup: {}
}

apiCreateGroup = (e, closeModal) => {
e.preventDefault();
if (this.validate()) {
api.StudentGroupApi.create(
(spe) => this.setState({ speCreateGroup: spe }),
{
title: this.state.formState.title
}
).then((group) => {
this.props.uiCreateStudentGroup(group);
closeModal();
});
}
}

validate = () => {
if (this.state.formState.title.length === 0) {
this.setState({ formValidation: { title: 'Please enter a title.' } });
return false;
} else {
return true;
}
}

render = () =>
<TogglerAndModal toggler={this.props.toggler}>{(closeModal) =>
<section className={"standard-modal standard-modal--sm"}>
<div className="standard-modal__header">
<h2 className="standard-modal__title">Create a new group</h2>
</div>

<div className="standard-modal__main">
<form className="standard-form -no-padding" onSubmit={(e) => this.apiCreateGroup(e, closeModal)}>
<div className="form-insides">
<TextInput formState={this.state.formState} updateFormState={(formState) => this.setState({ formState })} formValidation={this.state.formValidation} name="title" label="title" autoFocus/>
</div>

<button
className="button -purple standard-submit-button -move-up-on-hover"
type="submit"
style={disableOnSpeRequest(this.state.speCreateGroup, { opacity: 0.6 })}
>Create</button>
{/* <Loading spe={this.state.speSave}/> */}
</form>
</div>
</section>
}</TogglerAndModal>
}

export default CreateGroupModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import UserSelect from '~/appComponents/UserSelect';
import api from '~/api';
import CreateGroupModal from './components/CreateGroupModal';
import Loading from '~/components/Loading';

import css from './index.scss';

class OrganizeStudentsTab extends React.Component {
static propTypes = {
}

state = {
speStudentGroups: {},
selectedGroupId: null
}

componentDidMount = () => {
api.StudentGroupApi.getAll(
(spe) => this.setState({ speStudentGroups: spe })
)
.then(({ studentGroups }) => {
if (studentGroups[0]) {
this.setState({ selectedGroupId: studentGroups[0].id })
}
})
}

uiCreateStudentGroup = (studentGroup) => {
const spe = this.state.speStudentGroups;

this.setState({
speStudentGroups: {
...spe,
payload: {
...spe.payload,
studentGroups: [studentGroup, ...spe.payload.studentGroups]
}
}
});
}

render = () =>
<Loading spe={this.state.speStudentGroups}>{({ studentGroups, students }) =>
<div className={css.local}>
<div className="left">
<CreateGroupModal
toggler={<button type="button" className="button -white">Create group</button>}
uiCreateStudentGroup={this.uiCreateStudentGroup}
/>

<ul className="groups">
{studentGroups.map((group) =>
<li
className={`group ${this.state.selectedGroupId === group.id ? '-active' : ''}`}
key={group.id}
>
<button type="button" className="button -clear" onClick={() => this.setState({ selectedGroupId: group.id })}>
{group.title}
</button>
</li>
)}
</ul>
</div>

<div className="right">

<UserSelect onSelect={() => {}} placeholder="Add students..."/>

<ul className="students">
{students.map((student) =>
<li key={student.id}>{student.username}</li>
)}
</ul>
</div>
</div>
}</Loading>
}

export default OrganizeStudentsTab;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
:local(.local){
display: flex;
.left{
width: 40%;

ul.groups{
margin-top: 20px;

li.group{
&.-active{
button{ background: rgba(255, 255, 255, 0.08); }
}
&:hover{
button{ background: rgba(255, 255, 255, 0.05); }
}
button{
padding: 10px 7px;
border-radius: 2px;
max-width: 200px;

}
}
}
}
.right{
width: 60%;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import TogglerAndModal from '~/components/TogglerAndModal';
import TabNavigation from '~/components/TabNavigation';
import OrganizeStudentsTab from './components/OrganizeStudentsTab';

import { AuthenticationActions } from '~/reducers/Authentication';
import MyDuck from '~/ducks/MyDuck';

import css from './index.scss';

@connect(
(state) => ({
currentUser: state.global.Authentication.currentUser || false,
currentUser: state.global.Authentication.currentUser,
My: state.global.My
}),
(dispatch) => ({
Expand All @@ -21,10 +23,11 @@ class SettingsModal extends React.Component {
My: PropTypes.object.isRequired,
MyActions: PropTypes.object.isRequired,
signOut: PropTypes.func.isRequired,
currentUser: PropTypes.object.isRequired
}

state = {
selectedTab: 'Design',
selectedTab: 'Organize students',
hideSocialButtons: localStorage.getItem('hideSocialButtons') === 'true' ? true : false
}

Expand Down Expand Up @@ -52,13 +55,14 @@ class SettingsModal extends React.Component {
<TabNavigation
selectTab={(selectedTab) => this.setState({ selectedTab })}
selectedTab={this.state.selectedTab}
tabs={['Design', 'Manage']}
tabs={['Design', 'Organize students', 'Manage']}
/>

renderSelectedTab = () => {
return {
'Design': this.renderDesignTab,
'Manage': this.renderManageTab
'Manage': this.renderManageTab,
'Organize students': this.renderOrganizeStudentsTab
}[this.state.selectedTab]();
}

Expand All @@ -74,6 +78,9 @@ class SettingsModal extends React.Component {
</button>
</div>

renderOrganizeStudentsTab = () =>
<OrganizeStudentsTab currentUser={this.props.currentUser}/>

renderDesignTab = () =>
<div className="design-tab">
<section className="part-of-the-website">
Expand Down