Skip to content
Merged
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
96 changes: 48 additions & 48 deletions cypress/e2e/1.Login/login.cy.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,50 @@
// describe('Log In Feature: Test Invalid login', () => {
// it('should not log in', () => {
// cy.visit('/accounts/login/');
// cy.title().should('contain', 'Log in');
// // cy.get('#cu-privacy-notice-button').click();
// cy.get('#guest-login').click();
// cy.get('form[name="login_local"] div.login-local-form')
// .should('be.visible');
// cy.get('#id_username').type('foo');
// cy.get('#id_username').blur();
// cy.get('#id_password').type('foo');
// cy.get('#id_password').blur();
// cy.get('form[name="login_local"] button[type="submit"]').click();
// cy.title().should('contain', 'Log in');
// });
// });
describe('Log In Feature: Test Invalid login', () => {
it('should not log in', () => {
cy.visit('/accounts/login/');
cy.title().should('contain', 'Log in');
// cy.get('#cu-privacy-notice-button').click();
cy.get('#guest-login').click();
cy.get('form[name="login_local"] div.login-local-form')
.should('be.visible');
cy.get('#id_username').type('foo');
cy.get('#id_username').blur();
cy.get('#id_password').type('foo');
cy.get('#id_password').blur();
cy.get('form[name="login_local"] button[type="submit"]').click();
cy.title().should('contain', 'Log in');
});
});

// describe('Log In Feature: Test Instructor Login', () => {
// it('Logs in as faculty_one', () => {
// cy.visit('/accounts/login/');
// cy.title().should('contain', 'Log in');
// cy.get('#guest-login').click();
// cy.get('form[name="login_local"] div.login-local-form')
// .should('be.visible');
// cy.get('#id_username').type('faculty_one');
// cy.get('#id_username').blur();
// cy.get('#id_password').type('test');
// cy.get('#id_password').blur();
// cy.get('form[name="login_local"] button[type="submit"]').click();
// cy.title().should('contain', 'My Courses');
// cy.get('.navbar').should('contain', 'Faculty One');
// });
// });
describe('Log In Feature: Test Instructor Login', () => {
it('Logs in as faculty_one', () => {
cy.visit('/accounts/login/');
cy.title().should('contain', 'Log in');
cy.get('#guest-login').click();
cy.get('form[name="login_local"] div.login-local-form')
.should('be.visible');
cy.get('#id_username').type('faculty_one');
cy.get('#id_username').blur();
cy.get('#id_password').type('test');
cy.get('#id_password').blur();
cy.get('form[name="login_local"] button[type="submit"]').click();
cy.title().should('contain', 'My Courses');
cy.get('.navbar').should('contain', 'Faculty One');
});
});

// describe('Log In Feature: Test Student Login', () => {
// it('should test student login', () => {
// cy.visit('/accounts/login/');
// cy.title().should('contain', 'Log in');
// cy.get('#guest-login').click();
// cy.get('form[name="login_local"] div.login-local-form')
// .should('be.visible');
// cy.get('#id_username').type('faculty_one');
// cy.get('#id_username').blur();
// cy.get('#id_password').type('test');
// cy.get('#id_password').blur();
// cy.get('form[name="login_local"] button[type="submit"]').click();
// cy.title().should('contain', 'My Courses');
// cy.get('.navbar').should('contain', 'Faculty One');
// });
// });
describe('Log In Feature: Test Student Login', () => {
it('should test student login', () => {
cy.visit('/accounts/login/');
cy.title().should('contain', 'Log in');
cy.get('#guest-login').click();
cy.get('form[name="login_local"] div.login-local-form')
.should('be.visible');
cy.get('#id_username').type('faculty_one');
cy.get('#id_username').blur();
cy.get('#id_password').type('test');
cy.get('#id_password').blur();
cy.get('form[name="login_local"] button[type="submit"]').click();
cy.title().should('contain', 'My Courses');
cy.get('.navbar').should('contain', 'Faculty One');
});
});
29 changes: 29 additions & 0 deletions cypress/e2e/1.Login/visibility.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
describe('Visibility Controls', () => {
beforeEach(() => {
cy.resetTestDB();
});

it('Student does not see toggle buttons and respects visibility', () => {
cy.login('faculty_one', 'test');
cy.visit('/course/1/simulations/');

// Wait for page to load
cy.get('.section-sim-dashboard').should('be.visible');

// Verify NO toggle buttons
cy.contains('Show to Students').should('not.exist');
cy.contains('Hide from Students').should('not.exist');
});

it('Student does not see toggle buttons and respects visibility', () => {
cy.login('student_one', 'test');
cy.visit('/course/1/simulations/');

// Wait for page to load
cy.get('.section-sim-dashboard').should('be.visible');

// Verify NO toggle buttons
cy.contains('Show to Students').should('not.exist');
cy.contains('Hide from Students').should('not.exist');
});
})
2 changes: 0 additions & 2 deletions cypress/e2e/2.Sim1/navigate.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ describe('Navigate to Sim1 from login', () => {
cy.visit('/');
cy.title().should('contain', 'My Courses');
cy.get('[data-cy="navbar"]').should('contain', 'Faculty One');
cy.get('[data-cy="course-1"]')
.should('contain', 'course 0');
cy.get('[data-cy="course-1-link"]').click();
cy.get('[data-cy="sim-1"]').should('contain', 'Simulation 1');
cy.title().should('contain', 'Simulation');
Expand Down
26 changes: 21 additions & 5 deletions media/js/src/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,24 @@ const isSuperUser = window.MetricsMentor.currentUser.is_superuser;
const coursePk = getCoursePk();

export const App = () => {
const [initialVisibleSims, setInitialVisibleSims] = useState([]);
const [isFaculty, setIsFaculty] = useState(null);

useEffect(() => {
const appContainer = document.querySelector('#react-root');
const facultyStatus = appContainer ?
appContainer.dataset.isFaculty === 'True' : false;
setIsFaculty(facultyStatus);

if (appContainer && appContainer.dataset.visibleSimulations) {
try {
const parsed = JSON.parse(
appContainer.dataset.visibleSimulations);
setInitialVisibleSims(parsed);
} catch (e) {
console.error('Failed to parse visible simulations', e);
}
}
}, []);

if (isFaculty === null) {
Expand All @@ -30,20 +41,25 @@ export const App = () => {
<Route path='course/:courseId/simulations/'
element={<Dashboard
isSuperUser={isSuperUser}
isFaculty={isFaculty} />} />
{(isSuperUser || isFaculty || coursePk === 6) && (
isFaculty={isFaculty}
initialVisibleSims={initialVisibleSims} />} />
{(isSuperUser || isFaculty || coursePk === 6
|| initialVisibleSims.includes(1)) && (
<Route path='course/:courseId/simulations/1/'
element={<SimulationOne />} />
)}
{(isSuperUser || isFaculty || coursePk === 6) && (
{(isSuperUser || isFaculty || coursePk === 6
|| initialVisibleSims.includes(2)) && (
<Route path='course/:courseId/simulations/2/'
element={<SimulationTwo />} />
)}
{(isSuperUser || isFaculty || coursePk === 6) && (
{(isSuperUser || isFaculty || coursePk === 6
|| initialVisibleSims.includes(3)) && (
<Route path='course/:courseId/simulations/3/'
element={<SimulationThree />} />
)}
{(isSuperUser || isFaculty || coursePk === 6) && (
{(isSuperUser || isFaculty || coursePk === 6
|| initialVisibleSims.includes(4)) && (
<Route path='course/:courseId/simulations/4/'
element={<SimulationFour />} />
)}
Expand Down
56 changes: 49 additions & 7 deletions media/js/src/containers/dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,59 @@ import { useParams, Link } from 'react-router-dom';
import { Footer } from '../footer';
import PropTypes from 'prop-types';
import { Katex } from '../utils/katexComponent';
import { getCoursePk } from '../utils/utils';
import { getCoursePk, toggleVisibility } from '../utils/utils';
import { useState } from 'react';


export const Dashboard = ({ isSuperUser, isFaculty}) => {
export const Dashboard = ({ isSuperUser, isFaculty, initialVisibleSims }) => {

let { courseId } = useParams();
const coursePk = getCoursePk();
const [visibleSimulations, setVisibleSimulations] = useState(
initialVisibleSims || []
);

const handleToggle = async(simId) => {
const result = await toggleVisibility(coursePk, simId);
if (result.status === 'success') {
setVisibleSimulations(prev => {
if (result.is_visible) {
return [...prev, simId];
} else {
return prev.filter(id => id !== simId);
}
});
}
};

const isVisible = (simId) => {
if (isSuperUser || isFaculty) return true;
return visibleSimulations.includes(simId);
};

const renderToggle = (simId) => {
if (!isSuperUser) return null;
const isShown = visibleSimulations.includes(simId);
return (
<button
className={`btn btn-sm ${
isShown ? 'btn-outline-danger' : 'btn-outline-primary'
} mb-3`}
onClick={() => handleToggle(simId)}
>
{isShown ? 'Hide from Students' : 'Show to Students'}
</button>
);
};

return (
<>
<section className="section-sim-dashboard">
<div className="row">
{(isSuperUser || isFaculty || coursePk === 6) && (
{isVisible(1) && (
<div className="col-lg-5 p-4 mx-0 mx-lg-3 my-3 mx-lg-0
simulation-card">
{renderToggle(1)}
<h2 className="h2-primary">
<span className="h2-secondary d-block"
data-cy="sim-1">
Expand Down Expand Up @@ -46,9 +84,10 @@ export const Dashboard = ({ isSuperUser, isFaculty}) => {
</Link>
</div>
)}
{(isSuperUser || isFaculty || coursePk === 6) && (
{isVisible(2) && (
<div className="col-lg-5 p-4 mx-0 mx-lg-3 my-3 mx-lg-0
simulation-card">
{renderToggle(2)}
<h2 className="h2-primary">
<span className="h2-secondary d-block"
data-cy="sim-2">
Expand All @@ -73,9 +112,10 @@ export const Dashboard = ({ isSuperUser, isFaculty}) => {
</Link>
</div>
)}
{(isSuperUser || isFaculty || coursePk === 6) && (
{isVisible(3) && (
<div className="col-lg-5 p-4 mx-0 mx-lg-3 my-3 mx-lg-0
simulation-card">
{renderToggle(3)}
<h2 className="h2-primary">
<span className="h2-secondary d-block"
data-cy="sim-3">
Expand All @@ -102,9 +142,10 @@ export const Dashboard = ({ isSuperUser, isFaculty}) => {
</Link>
</div>
)}
{(isSuperUser || isFaculty || coursePk === 6) && (
{isVisible(4) && (
<div className="col-lg-5 p-4 mx-0 mx-lg-3 my-3 mx-lg-0
simulation-card">
{renderToggle(4)}
<h2 className="h2-primary">
<span className="h2-secondary d-block"
data-cy="sim-4">
Expand Down Expand Up @@ -138,5 +179,6 @@ export const Dashboard = ({ isSuperUser, isFaculty}) => {

Dashboard.propTypes = {
isSuperUser: PropTypes.bool,
isFaculty: PropTypes.bool
isFaculty: PropTypes.bool,
initialVisibleSims: PropTypes.array
};
19 changes: 19 additions & 0 deletions media/js/src/utils/utils.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,23 @@ export const createSubmission = async(
export const getCoursePk = () => {
const simContainer = document.querySelector('#react-root');
return simContainer ? Number(simContainer.dataset.course) : '';
};

/**
* Toggles visibility of a simulation.
* @param {number} coursePk
* @param {number} simulationId
* @returns {Promise<object>}
*/
export const toggleVisibility = async(coursePk, simulationId) => {
try {
const response = await authedFetch('/api/toggle-visibility/', 'POST', {
course_id: coursePk,
simulation_id: simulationId
});
return await response.json();
} catch (error) {
console.error('Error toggling visibility:', error);
return { status: 'error' };
}
};
26 changes: 26 additions & 0 deletions metricsmentor/main/migrations/0003_simulationvisibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('courseaffils', '0001_initial'),
('main', '0002_answer_active_quizsubmission_active'),
]

operations = [
migrations.CreateModel(
name='SimulationVisibility',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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')])),
('is_visible', models.BooleanField(default=False)),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courseaffils.Course')),
],
options={
'verbose_name_plural': 'Simulation Visibilities',
'unique_together': {('course', 'simulation')},
},
),
]
16 changes: 16 additions & 0 deletions metricsmentor/main/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,19 @@ class Answer(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
active = models.BooleanField(default=True)


class SimulationVisibility(models.Model):
course = models.ForeignKey(Course, on_delete=models.CASCADE)
simulation = models.IntegerField(choices=SIMULATIONS)
is_visible = models.BooleanField(default=False)

class Meta:
unique_together = ('course', 'simulation')
verbose_name_plural = "Simulation Visibilities"

def __str__(self):
return (
f"{self.course} - {self.get_simulation_display()} - "
f"{'Visible' if self.is_visible else 'Hidden'}"
)
Loading