Skip to content

Commit 1534de0

Browse files
committed
Merge remote-tracking branch 'upstream/unstable' into remove-vuetify-5670
2 parents 0c4bdf9 + 627ecf9 commit 1534de0

185 files changed

Lines changed: 13151 additions & 3077 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/dependabot.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version: 2
44
updates:
55

66
# Maintain dependencies for Python
7-
- package-ecosystem: "pip"
7+
- package-ecosystem: "uv"
88
directory: "/"
99
schedule:
1010
interval: "monthly"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: Handle pull request events
2+
on:
3+
pull_request_target:
4+
types: [review_requested, labeled]
5+
jobs:
6+
call-workflow:
7+
name: Call shared workflow
8+
uses: learningequality/.github/.github/workflows/pull-request-target.yml@main
9+
secrets:
10+
LE_BOT_APP_ID: ${{ secrets.LE_BOT_APP_ID }}
11+
LE_BOT_PRIVATE_KEY: ${{ secrets.LE_BOT_PRIVATE_KEY }}

.github/workflows/deploytest.yml

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,24 +48,16 @@ jobs:
4848
runs-on: ubuntu-latest
4949
steps:
5050
- uses: actions/checkout@v6
51-
- name: Set up Python 3.10
52-
uses: actions/setup-python@v6
51+
- name: Install uv
52+
uses: astral-sh/setup-uv@v7
5353
with:
5454
python-version: '3.10'
55-
- name: pip cache
56-
uses: actions/cache@v5
57-
with:
58-
path: ~/.cache/pip
59-
key: ${{ runner.os }}-pyprod-${{ hashFiles('requirements.txt') }}
60-
restore-keys: |
61-
${{ runner.os }}-pyprod-
62-
- name: Install pip-tools and python dependencies
55+
activate-environment: "true"
56+
enable-cache: "true"
57+
- name: Install python dependencies with uv
6358
run: |
64-
# Pin pip to 25.2 to avoid incompatibility with pip-tools and 25.3
65-
# see https://github.com/jazzband/pip-tools/issues/2252
66-
python -m pip install pip==25.2
67-
pip install pip-tools
68-
pip-sync requirements.txt
59+
# Use uv to install dependencies directly from requirements files
60+
uv pip sync requirements.txt
6961
- name: Use pnpm
7062
uses: pnpm/action-setup@v4
7163
- name: Use Node.js

.github/workflows/pre-commit.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ jobs:
3232
runs-on: ubuntu-latest
3333
steps:
3434
- uses: actions/checkout@v6
35-
- uses: actions/setup-python@v6
35+
- name: Install uv
36+
uses: astral-sh/setup-uv@v7
3637
with:
3738
python-version: '3.10'
39+
ignore-nothing-to-cache: 'true'
3840
- name: Use pnpm
3941
uses: pnpm/action-setup@v4
4042
- name: Use Node.js

.github/workflows/pythontest.yml

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -69,24 +69,16 @@ jobs:
6969
-e "MINIO_ROOT_PASSWORD=development" \
7070
-e "MINIO_DEFAULT_BUCKETS=content:public" \
7171
bitnamilegacy/minio:2024.5.28
72-
- name: Set up Python 3.10
73-
uses: actions/setup-python@v6
72+
- name: Install uv
73+
uses: astral-sh/setup-uv@v7
7474
with:
7575
python-version: '3.10'
76-
- name: pip cache
77-
uses: actions/cache@v5
78-
with:
79-
path: ~/.cache/pip
80-
key: ${{ runner.os }}-pytest-${{ hashFiles('requirements.txt', 'requirements-dev.txt') }}
81-
restore-keys: |
82-
${{ runner.os }}-pytest-
83-
- name: Install pip-tools and python dependencies
76+
activate-environment: "true"
77+
enable-cache: "true"
78+
- name: Install python dependencies with uv
8479
run: |
85-
# Pin pip to 25.2 to avoid incompatibility with pip-tools and 25.3
86-
# see https://github.com/jazzband/pip-tools/issues/2252
87-
python -m pip install pip==25.2
88-
pip install pip-tools
89-
pip-sync requirements.txt requirements-dev.txt
80+
# Use uv to install dependencies directly from requirements files
81+
uv pip sync requirements.txt requirements-dev.txt
9082
- name: Test pytest
9183
run: |
9284
sh -c './contentcuration/manage.py makemigrations --check'

.gitignore

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var/
2525

2626
# Ignore editor / IDE related data
2727
.vscode/
28+
.claude/
2829

2930
# IntelliJ IDE, except project config
3031
.idea/
@@ -129,6 +130,3 @@ storybook-static/
129130

130131
# i18n
131132
/contentcuration/locale/**/LC_MESSAGES/*.csv
132-
133-
# pyenv
134-
.python-version

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.10

Makefile

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# standalone install method
2-
DOCKER_COMPOSE = docker-compose
2+
DOCKER_COMPOSE ?= docker-compose
33

44
# support new plugin installation for docker-compose
5-
ifeq (, $(shell which docker-compose))
6-
DOCKER_COMPOSE = docker compose
5+
ifeq (, $(shell command -v docker-compose 2>/dev/null))
6+
DOCKER_COMPOSE := docker compose
77
endif
88

99
###############################################################
@@ -196,8 +196,8 @@ dctest: .docker/minio .docker/postgres
196196

197197
dcservicesup: .docker/minio .docker/postgres
198198
# launch all studio's dependent services using docker-compose
199-
$(DOCKER_COMPOSE) -f docker-compose.yml -f docker-compose.alt.yml up minio postgres redis
199+
$(DOCKER_COMPOSE) up minio postgres redis
200200

201201
dcservicesdown:
202202
# stop services that were started using dcservicesup
203-
$(DOCKER_COMPOSE) -f docker-compose.yml -f docker-compose.alt.yml down
203+
$(DOCKER_COMPOSE) down
Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,79 @@
1-
import { mount } from '@vue/test-utils';
1+
import { render, screen, fireEvent, waitFor } from '@testing-library/vue';
22
import VueRouter from 'vue-router';
33
import ForgotPassword from '../resetPassword/ForgotPassword';
44

5-
function makeWrapper() {
6-
return mount(ForgotPassword, {
7-
// Need to add a router instance as a child component relies on route linking
8-
router: new VueRouter(),
5+
const sendPasswordResetLinkMock = jest.fn(() => Promise.resolve());
6+
7+
const renderComponent = () => {
8+
const router = new VueRouter({
9+
routes: [
10+
{ path: '/', name: 'Main' },
11+
{ path: '/forgot-password', name: 'ForgotPassword' },
12+
{ path: '/password-instructions-sent', name: 'PasswordInstructionsSent' },
13+
],
14+
});
15+
16+
const utils = render(ForgotPassword, {
17+
router,
18+
store: {
19+
modules: {
20+
account: {
21+
namespaced: true,
22+
actions: {
23+
sendPasswordResetLink: sendPasswordResetLinkMock,
24+
},
25+
},
26+
},
27+
},
928
});
10-
}
1129

12-
describe('forgotPassword', () => {
13-
let wrapper;
14-
let sendPasswordResetLink;
30+
return { ...utils, router };
31+
};
1532

33+
describe('ForgotPassword', () => {
1634
beforeEach(() => {
17-
wrapper = makeWrapper();
18-
sendPasswordResetLink = jest.spyOn(wrapper.vm, 'sendPasswordResetLink');
19-
sendPasswordResetLink.mockImplementation(() => Promise.resolve());
20-
});
21-
it('should not call sendPasswordResetLink on submit if email is invalid', async () => {
22-
wrapper.findComponent({ ref: 'form' }).trigger('submit');
23-
await wrapper.vm.$nextTick();
24-
expect(sendPasswordResetLink).not.toHaveBeenCalled();
25-
});
26-
it('should call sendPasswordResetLink on submit if email is valid', async () => {
27-
wrapper.setData({ email: 'test@test.com' });
28-
await wrapper.vm.$nextTick();
29-
wrapper.findComponent({ ref: 'form' }).trigger('submit');
30-
await wrapper.vm.$nextTick();
31-
expect(sendPasswordResetLink).toHaveBeenCalled();
35+
jest.clearAllMocks();
36+
});
37+
38+
it('should render the forgot password form', () => {
39+
renderComponent();
40+
expect(screen.getByText('Reset your password')).toBeInTheDocument();
41+
expect(screen.getByLabelText('Email')).toBeInTheDocument();
42+
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
43+
});
44+
45+
it('should not submit form with empty email', async () => {
46+
renderComponent();
47+
await fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
48+
expect(sendPasswordResetLinkMock).not.toHaveBeenCalled();
49+
});
50+
51+
it('should not submit form with invalid email', async () => {
52+
renderComponent();
53+
await fireEvent.update(screen.getByLabelText(/email/i), 'invalid-email');
54+
await fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
55+
expect(sendPasswordResetLinkMock).not.toHaveBeenCalled();
56+
});
57+
58+
it('should submit form with valid email and navigate to success page', async () => {
59+
sendPasswordResetLinkMock.mockResolvedValue({});
60+
const { router } = renderComponent();
61+
await fireEvent.update(screen.getByLabelText(/email/i), 'test@test.com');
62+
await fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
63+
await waitFor(() => {
64+
expect(sendPasswordResetLinkMock).toHaveBeenCalledTimes(1);
65+
expect(sendPasswordResetLinkMock).toHaveBeenCalledWith(expect.anything(), 'test@test.com');
66+
});
67+
await waitFor(() => {
68+
expect(router.currentRoute.name).toBe('PasswordInstructionsSent');
69+
});
70+
});
71+
72+
it('should show error banner when submission fails', async () => {
73+
sendPasswordResetLinkMock.mockRejectedValue(new Error('Failed'));
74+
renderComponent();
75+
await fireEvent.update(screen.getByLabelText(/email/i), 'test@test.com');
76+
await fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
77+
expect(await screen.findByTestId('error-banner')).toBeInTheDocument();
3278
});
3379
});
Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,77 @@
1-
import { mount } from '@vue/test-utils';
1+
import { render, screen, waitFor } from '@testing-library/vue';
2+
import userEvent from '@testing-library/user-event';
3+
import Vuex from 'vuex';
4+
import Vue from 'vue';
25
import VueRouter from 'vue-router';
36
import RequestNewActivationLink from '../activateAccount/RequestNewActivationLink';
47

5-
function makeWrapper() {
6-
return mount(RequestNewActivationLink, {
7-
// Need to add a router instance as a child component relies on route linking
8-
router: new VueRouter(),
8+
Vue.use(Vuex);
9+
Vue.use(VueRouter);
10+
let testStore;
11+
12+
function createTestStore() {
13+
testStore = new Vuex.Store({
14+
modules: {
15+
account: {
16+
namespaced: true,
17+
actions: {
18+
sendActivationLink: jest.fn(() => Promise.resolve()),
19+
},
20+
},
21+
},
922
});
23+
return testStore;
1024
}
1125

12-
describe('requestNewActivationLink', () => {
13-
let wrapper;
14-
let sendActivationLink;
26+
function renderComponent() {
27+
const router = new VueRouter({
28+
routes: [
29+
{
30+
path: '/main',
31+
name: 'Main',
32+
},
33+
{
34+
path: '/activation-link-resent',
35+
name: 'ActivationLinkReSent',
36+
},
37+
],
38+
});
1539

16-
beforeEach(() => {
17-
wrapper = makeWrapper();
18-
sendActivationLink = jest.spyOn(wrapper.vm, 'sendActivationLink');
19-
sendActivationLink.mockImplementation(() => Promise.resolve());
40+
return render(RequestNewActivationLink, {
41+
store: createTestStore(),
42+
router,
2043
});
44+
}
45+
46+
describe('requestNewActivationLink', () => {
47+
it('should show validation error when submitting with invalid email', async () => {
48+
const user = userEvent.setup();
49+
renderComponent();
50+
51+
const submitButton = screen.getByRole('button', { name: /submit/i });
52+
await user.click(submitButton);
2153

22-
it('should not call sendActivationLink on submit if email is invalid', async () => {
23-
await wrapper.findComponent({ ref: 'form' }).trigger('submit');
24-
expect(sendActivationLink).not.toHaveBeenCalled();
54+
await waitFor(() => {
55+
expect(screen.getByText(/activation failed/i)).toBeInTheDocument();
56+
});
2557
});
2658

27-
it('should call sendActivationLink on submit if email is valid', async () => {
28-
await wrapper.setData({ email: 'test@test.com' });
29-
await wrapper.findComponent({ ref: 'form' }).trigger('submit');
30-
expect(sendActivationLink).toHaveBeenCalled();
59+
it('should submit when email is valid', async () => {
60+
const user = userEvent.setup();
61+
renderComponent();
62+
const sendActivationLink = jest.spyOn(testStore, 'dispatch');
63+
64+
const emailInput = screen.getByLabelText(/email/i);
65+
const submitButton = screen.getByRole('button', { name: /submit/i });
66+
67+
await user.type(emailInput, 'test@test.com');
68+
await user.click(submitButton);
69+
70+
await waitFor(() => {
71+
expect(sendActivationLink).toHaveBeenCalledWith(
72+
'account/sendActivationLink',
73+
'test@test.com',
74+
);
75+
});
3176
});
3277
});

0 commit comments

Comments
 (0)