Skip to content

Commit 4796d33

Browse files
authored
Merge pull request #1203 from ccnmtl/ERD-457
Add sim toggle
2 parents 5719854 + 6bb35f3 commit 4796d33

File tree

12 files changed

+332
-66
lines changed

12 files changed

+332
-66
lines changed

cypress/e2e/1.Login/login.cy.js

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,50 @@
1-
// describe('Log In Feature: Test Invalid login', () => {
2-
// it('should not log in', () => {
3-
// cy.visit('/accounts/login/');
4-
// cy.title().should('contain', 'Log in');
5-
// // cy.get('#cu-privacy-notice-button').click();
6-
// cy.get('#guest-login').click();
7-
// cy.get('form[name="login_local"] div.login-local-form')
8-
// .should('be.visible');
9-
// cy.get('#id_username').type('foo');
10-
// cy.get('#id_username').blur();
11-
// cy.get('#id_password').type('foo');
12-
// cy.get('#id_password').blur();
13-
// cy.get('form[name="login_local"] button[type="submit"]').click();
14-
// cy.title().should('contain', 'Log in');
15-
// });
16-
// });
1+
describe('Log In Feature: Test Invalid login', () => {
2+
it('should not log in', () => {
3+
cy.visit('/accounts/login/');
4+
cy.title().should('contain', 'Log in');
5+
// cy.get('#cu-privacy-notice-button').click();
6+
cy.get('#guest-login').click();
7+
cy.get('form[name="login_local"] div.login-local-form')
8+
.should('be.visible');
9+
cy.get('#id_username').type('foo');
10+
cy.get('#id_username').blur();
11+
cy.get('#id_password').type('foo');
12+
cy.get('#id_password').blur();
13+
cy.get('form[name="login_local"] button[type="submit"]').click();
14+
cy.title().should('contain', 'Log in');
15+
});
16+
});
1717

18-
// describe('Log In Feature: Test Instructor Login', () => {
19-
// it('Logs in as faculty_one', () => {
20-
// cy.visit('/accounts/login/');
21-
// cy.title().should('contain', 'Log in');
22-
// cy.get('#guest-login').click();
23-
// cy.get('form[name="login_local"] div.login-local-form')
24-
// .should('be.visible');
25-
// cy.get('#id_username').type('faculty_one');
26-
// cy.get('#id_username').blur();
27-
// cy.get('#id_password').type('test');
28-
// cy.get('#id_password').blur();
29-
// cy.get('form[name="login_local"] button[type="submit"]').click();
30-
// cy.title().should('contain', 'My Courses');
31-
// cy.get('.navbar').should('contain', 'Faculty One');
32-
// });
33-
// });
18+
describe('Log In Feature: Test Instructor Login', () => {
19+
it('Logs in as faculty_one', () => {
20+
cy.visit('/accounts/login/');
21+
cy.title().should('contain', 'Log in');
22+
cy.get('#guest-login').click();
23+
cy.get('form[name="login_local"] div.login-local-form')
24+
.should('be.visible');
25+
cy.get('#id_username').type('faculty_one');
26+
cy.get('#id_username').blur();
27+
cy.get('#id_password').type('test');
28+
cy.get('#id_password').blur();
29+
cy.get('form[name="login_local"] button[type="submit"]').click();
30+
cy.title().should('contain', 'My Courses');
31+
cy.get('.navbar').should('contain', 'Faculty One');
32+
});
33+
});
3434

35-
// describe('Log In Feature: Test Student Login', () => {
36-
// it('should test student login', () => {
37-
// cy.visit('/accounts/login/');
38-
// cy.title().should('contain', 'Log in');
39-
// cy.get('#guest-login').click();
40-
// cy.get('form[name="login_local"] div.login-local-form')
41-
// .should('be.visible');
42-
// cy.get('#id_username').type('faculty_one');
43-
// cy.get('#id_username').blur();
44-
// cy.get('#id_password').type('test');
45-
// cy.get('#id_password').blur();
46-
// cy.get('form[name="login_local"] button[type="submit"]').click();
47-
// cy.title().should('contain', 'My Courses');
48-
// cy.get('.navbar').should('contain', 'Faculty One');
49-
// });
50-
// });
35+
describe('Log In Feature: Test Student Login', () => {
36+
it('should test student login', () => {
37+
cy.visit('/accounts/login/');
38+
cy.title().should('contain', 'Log in');
39+
cy.get('#guest-login').click();
40+
cy.get('form[name="login_local"] div.login-local-form')
41+
.should('be.visible');
42+
cy.get('#id_username').type('faculty_one');
43+
cy.get('#id_username').blur();
44+
cy.get('#id_password').type('test');
45+
cy.get('#id_password').blur();
46+
cy.get('form[name="login_local"] button[type="submit"]').click();
47+
cy.title().should('contain', 'My Courses');
48+
cy.get('.navbar').should('contain', 'Faculty One');
49+
});
50+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
describe('Visibility Controls', () => {
2+
beforeEach(() => {
3+
cy.resetTestDB();
4+
});
5+
6+
it('Student does not see toggle buttons and respects visibility', () => {
7+
cy.login('faculty_one', 'test');
8+
cy.visit('/course/1/simulations/');
9+
10+
// Wait for page to load
11+
cy.get('.section-sim-dashboard').should('be.visible');
12+
13+
// Verify NO toggle buttons
14+
cy.contains('Show to Students').should('not.exist');
15+
cy.contains('Hide from Students').should('not.exist');
16+
});
17+
18+
it('Student does not see toggle buttons and respects visibility', () => {
19+
cy.login('student_one', 'test');
20+
cy.visit('/course/1/simulations/');
21+
22+
// Wait for page to load
23+
cy.get('.section-sim-dashboard').should('be.visible');
24+
25+
// Verify NO toggle buttons
26+
cy.contains('Show to Students').should('not.exist');
27+
cy.contains('Hide from Students').should('not.exist');
28+
});
29+
})

cypress/e2e/2.Sim1/navigate.cy.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ describe('Navigate to Sim1 from login', () => {
44
cy.visit('/');
55
cy.title().should('contain', 'My Courses');
66
cy.get('[data-cy="navbar"]').should('contain', 'Faculty One');
7-
cy.get('[data-cy="course-1"]')
8-
.should('contain', 'course 0');
97
cy.get('[data-cy="course-1-link"]').click();
108
cy.get('[data-cy="sim-1"]').should('contain', 'Simulation 1');
119
cy.title().should('contain', 'Simulation');

media/js/src/app.jsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,24 @@ const isSuperUser = window.MetricsMentor.currentUser.is_superuser;
1111
const coursePk = getCoursePk();
1212

1313
export const App = () => {
14+
const [initialVisibleSims, setInitialVisibleSims] = useState([]);
1415
const [isFaculty, setIsFaculty] = useState(null);
1516

1617
useEffect(() => {
1718
const appContainer = document.querySelector('#react-root');
1819
const facultyStatus = appContainer ?
1920
appContainer.dataset.isFaculty === 'True' : false;
2021
setIsFaculty(facultyStatus);
22+
23+
if (appContainer && appContainer.dataset.visibleSimulations) {
24+
try {
25+
const parsed = JSON.parse(
26+
appContainer.dataset.visibleSimulations);
27+
setInitialVisibleSims(parsed);
28+
} catch (e) {
29+
console.error('Failed to parse visible simulations', e);
30+
}
31+
}
2132
}, []);
2233

2334
if (isFaculty === null) {
@@ -30,20 +41,25 @@ export const App = () => {
3041
<Route path='course/:courseId/simulations/'
3142
element={<Dashboard
3243
isSuperUser={isSuperUser}
33-
isFaculty={isFaculty} />} />
34-
{(isSuperUser || isFaculty || coursePk === 6) && (
44+
isFaculty={isFaculty}
45+
initialVisibleSims={initialVisibleSims} />} />
46+
{(isSuperUser || isFaculty || coursePk === 6
47+
|| initialVisibleSims.includes(1)) && (
3548
<Route path='course/:courseId/simulations/1/'
3649
element={<SimulationOne />} />
3750
)}
38-
{(isSuperUser || isFaculty || coursePk === 6) && (
51+
{(isSuperUser || isFaculty || coursePk === 6
52+
|| initialVisibleSims.includes(2)) && (
3953
<Route path='course/:courseId/simulations/2/'
4054
element={<SimulationTwo />} />
4155
)}
42-
{(isSuperUser || isFaculty || coursePk === 6) && (
56+
{(isSuperUser || isFaculty || coursePk === 6
57+
|| initialVisibleSims.includes(3)) && (
4358
<Route path='course/:courseId/simulations/3/'
4459
element={<SimulationThree />} />
4560
)}
46-
{(isSuperUser || isFaculty || coursePk === 6) && (
61+
{(isSuperUser || isFaculty || coursePk === 6
62+
|| initialVisibleSims.includes(4)) && (
4763
<Route path='course/:courseId/simulations/4/'
4864
element={<SimulationFour />} />
4965
)}

media/js/src/containers/dashboard.jsx

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,59 @@ import { useParams, Link } from 'react-router-dom';
33
import { Footer } from '../footer';
44
import PropTypes from 'prop-types';
55
import { Katex } from '../utils/katexComponent';
6-
import { getCoursePk } from '../utils/utils';
6+
import { getCoursePk, toggleVisibility } from '../utils/utils';
7+
import { useState } from 'react';
78

89

9-
export const Dashboard = ({ isSuperUser, isFaculty}) => {
10+
export const Dashboard = ({ isSuperUser, isFaculty, initialVisibleSims }) => {
1011

1112
let { courseId } = useParams();
1213
const coursePk = getCoursePk();
14+
const [visibleSimulations, setVisibleSimulations] = useState(
15+
initialVisibleSims || []
16+
);
17+
18+
const handleToggle = async(simId) => {
19+
const result = await toggleVisibility(coursePk, simId);
20+
if (result.status === 'success') {
21+
setVisibleSimulations(prev => {
22+
if (result.is_visible) {
23+
return [...prev, simId];
24+
} else {
25+
return prev.filter(id => id !== simId);
26+
}
27+
});
28+
}
29+
};
30+
31+
const isVisible = (simId) => {
32+
if (isSuperUser || isFaculty) return true;
33+
return visibleSimulations.includes(simId);
34+
};
35+
36+
const renderToggle = (simId) => {
37+
if (!isSuperUser) return null;
38+
const isShown = visibleSimulations.includes(simId);
39+
return (
40+
<button
41+
className={`btn btn-sm ${
42+
isShown ? 'btn-outline-danger' : 'btn-outline-primary'
43+
} mb-3`}
44+
onClick={() => handleToggle(simId)}
45+
>
46+
{isShown ? 'Hide from Students' : 'Show to Students'}
47+
</button>
48+
);
49+
};
1350

1451
return (
1552
<>
1653
<section className="section-sim-dashboard">
1754
<div className="row">
18-
{(isSuperUser || isFaculty || coursePk === 6) && (
55+
{isVisible(1) && (
1956
<div className="col-lg-5 p-4 mx-0 mx-lg-3 my-3 mx-lg-0
2057
simulation-card">
58+
{renderToggle(1)}
2159
<h2 className="h2-primary">
2260
<span className="h2-secondary d-block"
2361
data-cy="sim-1">
@@ -46,9 +84,10 @@ export const Dashboard = ({ isSuperUser, isFaculty}) => {
4684
</Link>
4785
</div>
4886
)}
49-
{(isSuperUser || isFaculty || coursePk === 6) && (
87+
{isVisible(2) && (
5088
<div className="col-lg-5 p-4 mx-0 mx-lg-3 my-3 mx-lg-0
5189
simulation-card">
90+
{renderToggle(2)}
5291
<h2 className="h2-primary">
5392
<span className="h2-secondary d-block"
5493
data-cy="sim-2">
@@ -73,9 +112,10 @@ export const Dashboard = ({ isSuperUser, isFaculty}) => {
73112
</Link>
74113
</div>
75114
)}
76-
{(isSuperUser || isFaculty || coursePk === 6) && (
115+
{isVisible(3) && (
77116
<div className="col-lg-5 p-4 mx-0 mx-lg-3 my-3 mx-lg-0
78117
simulation-card">
118+
{renderToggle(3)}
79119
<h2 className="h2-primary">
80120
<span className="h2-secondary d-block"
81121
data-cy="sim-3">
@@ -102,9 +142,10 @@ export const Dashboard = ({ isSuperUser, isFaculty}) => {
102142
</Link>
103143
</div>
104144
)}
105-
{(isSuperUser || isFaculty || coursePk === 6) && (
145+
{isVisible(4) && (
106146
<div className="col-lg-5 p-4 mx-0 mx-lg-3 my-3 mx-lg-0
107147
simulation-card">
148+
{renderToggle(4)}
108149
<h2 className="h2-primary">
109150
<span className="h2-secondary d-block"
110151
data-cy="sim-4">
@@ -138,5 +179,6 @@ export const Dashboard = ({ isSuperUser, isFaculty}) => {
138179

139180
Dashboard.propTypes = {
140181
isSuperUser: PropTypes.bool,
141-
isFaculty: PropTypes.bool
182+
isFaculty: PropTypes.bool,
183+
initialVisibleSims: PropTypes.array
142184
};

media/js/src/utils/utils.jsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,4 +252,23 @@ export const createSubmission = async(
252252
export const getCoursePk = () => {
253253
const simContainer = document.querySelector('#react-root');
254254
return simContainer ? Number(simContainer.dataset.course) : '';
255+
};
256+
257+
/**
258+
* Toggles visibility of a simulation.
259+
* @param {number} coursePk
260+
* @param {number} simulationId
261+
* @returns {Promise<object>}
262+
*/
263+
export const toggleVisibility = async(coursePk, simulationId) => {
264+
try {
265+
const response = await authedFetch('/api/toggle-visibility/', 'POST', {
266+
course_id: coursePk,
267+
simulation_id: simulationId
268+
});
269+
return await response.json();
270+
} catch (error) {
271+
console.error('Error toggling visibility:', error);
272+
return { status: 'error' };
273+
}
255274
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from django.db import migrations, models
2+
import django.db.models.deletion
3+
4+
5+
class Migration(migrations.Migration):
6+
7+
dependencies = [
8+
('courseaffils', '0001_initial'),
9+
('main', '0002_answer_active_quizsubmission_active'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='SimulationVisibility',
15+
fields=[
16+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17+
('simulation', models.IntegerField(choices=[(1, 'Simulation 1'), (2, 'Simulation 2'), (3, 'Simulation 3'), (4, 'Simulation 4'), (5, 'Simulation 5'), (6, 'Simulation 6'), (7, 'Simulation 7')])),
18+
('is_visible', models.BooleanField(default=False)),
19+
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courseaffils.Course')),
20+
],
21+
options={
22+
'verbose_name_plural': 'Simulation Visibilities',
23+
'unique_together': {('course', 'simulation')},
24+
},
25+
),
26+
]

metricsmentor/main/models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,19 @@ class Answer(models.Model):
3434
created_at = models.DateTimeField(auto_now_add=True)
3535
updated_at = models.DateTimeField(auto_now=True)
3636
active = models.BooleanField(default=True)
37+
38+
39+
class SimulationVisibility(models.Model):
40+
course = models.ForeignKey(Course, on_delete=models.CASCADE)
41+
simulation = models.IntegerField(choices=SIMULATIONS)
42+
is_visible = models.BooleanField(default=False)
43+
44+
class Meta:
45+
unique_together = ('course', 'simulation')
46+
verbose_name_plural = "Simulation Visibilities"
47+
48+
def __str__(self):
49+
return (
50+
f"{self.course} - {self.get_simulation_display()} - "
51+
f"{'Visible' if self.is_visible else 'Hidden'}"
52+
)

0 commit comments

Comments
 (0)