diff --git a/.docker/README.md b/.docker/README.md new file mode 100644 index 0000000000..66b76b0f2d --- /dev/null +++ b/.docker/README.md @@ -0,0 +1,17 @@ +## What is this directory? +This directory is a space for mounting directories to docker containers, allowing the mounts to be specified in committed code, but the contents of the mounts to remain ignored by git. + +### postgres +The `postgres` directory is mounted to `/docker-entrypoint-initdb.d`. Any `.sh` or `.sql` files will be executed when the container is first started with a new data volume. You may read more regarding this functionality on the [Docker Hub page](https://hub.docker.com/_/postgres), under _Initialization scripts_. + +When running docker services through the Makefile commands, it specifies a docker-compose project name that depends on the name of the current git branch. This causes the volumes to change when the branch changes, which is helpful when switching between many branches that might have incompatible database schema changes. The downside is that whenever you start a new branch, you'll have to re-initialize the database again, like with `yarn run devsetup`. Creating a SQL dump from an existing, initialized database and placing it in this directory will allow you to skip this step. + +To create a SQL dump of your preferred database data useful for local testing, run `make .docker/postgres/init.sql` while the docker postgres container is running. + +> Note: you will likely need to run `make migrate` to ensure your database schema is up-to-date when using this technique. + +#### pgpass +Stores the postgres authentication for the docker service for scripting access without manually providing a password, created by `make .docker/pgpass` + +### minio +The `minio` directory is mounted to `/data`, since it isn't necessarily useful to have this data isolated based off the current git branch. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 128ae4fe41..663fdde54c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,9 +8,18 @@ updates: directory: "/" schedule: interval: "daily" + time: "00:00" # Maintain dependencies for Javascript - package-ecosystem: "npm" directory: "/" schedule: interval: "daily" + time: "00:00" + + # Maintain dependencies for Github Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + time: "00:00" diff --git a/.github/workflows/deploytest.yml b/.github/workflows/deploytest.yml index 11768ed489..d63794aaeb 100644 --- a/.github/workflows/deploytest.yml +++ b/.github/workflows/deploytest.yml @@ -27,13 +27,13 @@ jobs: if: ${{ needs.pre_job.outputs.should_skip != 'true' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: '16.x' - name: Cache Node.js modules - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: '**/node_modules' key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} @@ -51,13 +51,13 @@ jobs: if: ${{ needs.pre_job.outputs.should_skip != 'true' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.9 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.9 - name: pip cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.cache/pip key: ${{ runner.os }}-pyprod-${{ hashFiles('requirements.txt') }} @@ -69,11 +69,11 @@ jobs: pip install pip-tools pip-sync requirements.txt - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: '16.x' - name: Cache Node.js modules - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: '**/node_modules' key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} diff --git a/.github/workflows/frontendlint.yml b/.github/workflows/frontendlint.yml index 6de4d701bd..d0a3e648f2 100644 --- a/.github/workflows/frontendlint.yml +++ b/.github/workflows/frontendlint.yml @@ -27,13 +27,13 @@ jobs: if: ${{ needs.pre_job.outputs.should_skip != 'true' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: '16.x' - name: Cache Node.js modules - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: '**/node_modules' key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} diff --git a/.github/workflows/frontendtest.yml b/.github/workflows/frontendtest.yml index c9ed46672c..7489c2748a 100644 --- a/.github/workflows/frontendtest.yml +++ b/.github/workflows/frontendtest.yml @@ -27,13 +27,13 @@ jobs: if: ${{ needs.pre_job.outputs.should_skip != 'true' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: '16.x' - name: Cache Node.js modules - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: '**/node_modules' key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} diff --git a/.github/workflows/pythontest.yml b/.github/workflows/pythontest.yml index 217399ea8e..f015f7bc1e 100644 --- a/.github/workflows/pythontest.yml +++ b/.github/workflows/pythontest.yml @@ -61,7 +61,7 @@ jobs: # Maps port 6379 on service container to the host - 6379:6379 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up minio run: | docker run -d -p 9000:9000 --name minio \ @@ -71,11 +71,11 @@ jobs: -v /tmp/minio_config:/root/.minio \ minio/minio server /data - name: Set up Python 3.9 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.9 - name: pip cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.cache/pip key: ${{ runner.os }}-pytest-${{ hashFiles('requirements.txt', 'requirements-dev.txt') }} diff --git a/.gitignore b/.gitignore index b5e0261f09..8d869357f8 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,8 @@ var/ # IntelliJ IDE, except project config .idea/* !.idea/studio.iml +# ignore future updates to run configuration +.run/devserver.run.xml # PyInstaller # Usually these files are written by a python script from a template @@ -95,8 +97,11 @@ contentcuration/csvs/ # Ignore the TAGS file generated by some editors TAGS -# Ignore Vagrant-created files -/.vagrant/ +# Services +.vagrant/ +.docker/minio/* +.docker/postgres/* +.docker/pgpass # Ignore test files /contentcuration/contentcuration/proxy_settings.py diff --git a/.run/devserver.run.xml b/.run/devserver.run.xml new file mode 100644 index 0000000000..1c94ee6402 --- /dev/null +++ b/.run/devserver.run.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/Makefile b/Makefile index 29fe984285..6f7b7e6cec 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,11 @@ +# standalone install method +DOCKER_COMPOSE = docker-compose + +# support new plugin installation for docker-compose +ifeq (, $(shell which docker-compose)) +DOCKER_COMPOSE = docker compose +endif + ############################################################### # PRODUCTION COMMANDS ######################################### ############################################################### @@ -20,6 +28,18 @@ migrate: python contentcuration/manage.py migrate || true python contentcuration/manage.py loadconstants +# This is a special command that is we'll reuse to run data migrations outside of the normal +# django migration process. This is useful for long running migrations which we don't want to block +# the CD build. Do not delete! +# Procedure: +# 1) Add a new management command for the migration +# 2) Call it here +# 3) Perform the release +# 4) Remove the management command from this `deploy-migrate` recipe +# 5) Repeat! +deploy-migrate: + echo "Nothing to do here" + contentnodegc: python contentcuration/manage.py garbage_collect @@ -31,7 +51,11 @@ learningactivities: set-tsvectors: python contentcuration/manage.py set_channel_tsvectors - python contentcuration/manage.py set_contentnode_tsvectors + python contentcuration/manage.py set_contentnode_tsvectors --published + +reconcile: + python contentcuration/manage.py reconcile_publishing_status + python contentcuration/manage.py reconcile_change_tasks ############################################################### # END PRODUCTION COMMANDS ##################################### @@ -53,10 +77,10 @@ i18n-extract: i18n-extract-frontend i18n-extract-backend i18n-transfer-context: yarn transfercontext -#i18n-django-compilemessages: - # Change working directory to kolibri/ such that compilemessages +i18n-django-compilemessages: + # Change working directory to contentcuration/ such that compilemessages # finds only the .po files nested there. - #cd kolibri && PYTHONPATH="..:$$PYTHONPATH" python -m kolibri manage compilemessages + cd contentcuration && python manage.py compilemessages i18n-upload: i18n-extract python node_modules/kolibri-tools/lib/i18n/crowdin.py upload-sources ${branch} @@ -67,27 +91,15 @@ i18n-pretranslate: i18n-pretranslate-approve-all: python node_modules/kolibri-tools/lib/i18n/crowdin.py pretranslate ${branch} --approve-all -i18n-convert: - python node_modules/kolibri-tools/lib/i18n/crowdin.py convert-files - i18n-download-translations: python node_modules/kolibri-tools/lib/i18n/crowdin.py rebuild-translations ${branch} python node_modules/kolibri-tools/lib/i18n/crowdin.py download-translations ${branch} - node node_modules/kolibri-tools/lib/i18n/intl_code_gen.js - python node_modules/kolibri-tools/lib/i18n/crowdin.py convert-files - # TODO: is this necessary? # Manual hack to add es language by copying es_ES to es - # cp -r contentcuration/locale/es_ES contentcuration/locale/es + yarn exec kolibri-tools i18n-code-gen -- --output-dir ./contentcuration/contentcuration/frontend/shared/i18n + $(MAKE) i18n-django-compilemessages + yarn exec kolibri-tools i18n-create-message-files -- --namespace contentcuration --searchPath ./contentcuration/contentcuration/frontend i18n-download: i18n-download-translations -i18n-update: - echo "WARNING: i18n-update has been renamed to i18n-download" - $(MAKE) i18n-download - echo "WARNING: i18n-update has been renamed to i18n-download" - -i18n-stats: - python node_modules/kolibri-tools/lib/i18n/crowdin.py translation-stats ${branch} - i18n-download-glossary: python node_modules/kolibri-tools/lib/i18n/crowdin.py download-glossary @@ -126,9 +138,9 @@ hascaptions: export COMPOSE_PROJECT_NAME=studio_$(shell git rev-parse --abbrev-ref HEAD) -purge-postgres: - -PGPASSWORD=kolibri dropdb -U learningequality "kolibri-studio" --port 5432 -h localhost - PGPASSWORD=kolibri createdb -U learningequality "kolibri-studio" --port 5432 -h localhost +purge-postgres: .docker/pgpass + -PGPASSFILE=.docker/pgpass dropdb -U learningequality "kolibri-studio" --port 5432 -h localhost + PGPASSFILE=.docker/pgpass createdb -U learningequality "kolibri-studio" --port 5432 -h localhost destroy-and-recreate-database: purge-postgres setup @@ -138,39 +150,56 @@ devceleryworkers: run-services: $(MAKE) -j 2 dcservicesup devceleryworkers +.docker/minio: + mkdir -p $@ + +.docker/postgres: + mkdir -p $@ + +.docker/pgpass: + echo "localhost:5432:kolibri-studio:learningequality:kolibri" > $@ + chmod 600 $@ + +.docker/postgres/init.sql: .docker/pgpass + # assumes postgres is running in a docker container + PGPASSFILE=.docker/pgpass pg_dump --host localhost --port 5432 --username learningequality --dbname "kolibri-studio" --exclude-table-data=contentcuration_change --file $@ + dcbuild: # build all studio docker image and all dependent services using docker-compose - docker-compose build + $(DOCKER_COMPOSE) build -dcup: +dcup: .docker/minio .docker/postgres # run all services except for cloudprober - docker-compose up studio-app celery-worker + $(DOCKER_COMPOSE) up studio-app celery-worker -dcup-cloudprober: +dcup-cloudprober: .docker/minio .docker/postgres # run all services including cloudprober - docker-compose up + $(DOCKER_COMPOSE) up dcdown: - # run make deverver in foreground with all dependent services using docker-compose - docker-compose down + # run make deverver in foreground with all dependent services using $(DOCKER_COMPOSE) + $(DOCKER_COMPOSE) down dcclean: # stop all containers and delete volumes - docker-compose down -v + $(DOCKER_COMPOSE) down -v docker image prune -f dcshell: # bash shell inside the (running!) studio-app container - docker-compose exec studio-app /usr/bin/fish + $(DOCKER_COMPOSE) exec studio-app /usr/bin/fish + +dcpsql: .docker/pgpass + PGPASSFILE=.docker/pgpass psql --host localhost --port 5432 --username learningequality --dbname "kolibri-studio" -dctest: +dctest: .docker/minio .docker/postgres # run backend tests inside docker, in new instances - docker-compose run studio-app make test + $(DOCKER_COMPOSE) run studio-app make test -dcservicesup: +dcservicesup: .docker/minio .docker/postgres # launch all studio's dependent services using docker-compose - docker-compose -f docker-compose.yml -f docker-compose.alt.yml up minio postgres redis + $(DOCKER_COMPOSE) -f docker-compose.yml -f docker-compose.alt.yml up minio postgres redis dcservicesdown: # stop services that were started using dcservicesup - docker-compose -f docker-compose.yml -f docker-compose.alt.yml down + $(DOCKER_COMPOSE) -f docker-compose.yml -f docker-compose.alt.yml down diff --git a/README.md b/README.md index 821aba3b88..aa0a9ad8d5 100644 --- a/README.md +++ b/README.md @@ -59,10 +59,10 @@ export LDFLAGS="-L/opt/homebrew/opt/openssl/lib" ``` ### Install frontend dependencies -Install the version of node.js supported by Studio, and install `yarn`: +Install the version of node.js supported by Studio, and install `yarn` version 1.x: ```bash volta install node@16 -volta install yarn +volta install yarn@1 ``` After installing `yarn`, you may now install frontend dependencies: ```bash diff --git a/contentcuration/contentcuration/api.py b/contentcuration/contentcuration/api.py index 33c9692cbc..b297ffaba6 100644 --- a/contentcuration/contentcuration/api.py +++ b/contentcuration/contentcuration/api.py @@ -10,9 +10,6 @@ from django.core.files.storage import default_storage import contentcuration.models as models -from contentcuration.utils.garbage_collect import get_deleted_chefs_root -from contentcuration.viewsets.sync.constants import CHANNEL -from contentcuration.viewsets.sync.utils import generate_update_event def write_file_to_storage(fobj, check_valid=False, name=None): @@ -68,33 +65,3 @@ def get_hash(fobj): md5.update(chunk) fobj.seek(0) return md5.hexdigest() - - -def activate_channel(channel, user): - user.check_channel_space(channel) - - if channel.previous_tree and channel.previous_tree != channel.main_tree: - # IMPORTANT: Do not remove this block, MPTT updating the deleted chefs block could hang the server - with models.ContentNode.objects.disable_mptt_updates(): - garbage_node = get_deleted_chefs_root() - channel.previous_tree.parent = garbage_node - channel.previous_tree.title = "Previous tree for channel {}".format(channel.pk) - channel.previous_tree.save() - - channel.previous_tree = channel.main_tree - channel.main_tree = channel.staging_tree - channel.staging_tree = None - channel.save() - - user.staged_files.all().delete() - user.set_space_used() - - models.Change.create_change(generate_update_event( - channel.id, - CHANNEL, - { - "root_id": channel.main_tree.id, - "staging_root_id": None - }, - channel_id=channel.id, - ), applied=True, created_by_id=user.id) diff --git a/contentcuration/contentcuration/constants/channel_history.py b/contentcuration/contentcuration/constants/channel_history.py index 28de05e035..790b4dfd51 100644 --- a/contentcuration/contentcuration/constants/channel_history.py +++ b/contentcuration/contentcuration/constants/channel_history.py @@ -1,13 +1,11 @@ -from django.utils.translation import ugettext_lazy as _ - CREATION = "creation" PUBLICATION = "publication" DELETION = "deletion" RECOVERY = "recovery" choices = ( - (CREATION, _("Creation")), - (PUBLICATION, _("Publication")), - (DELETION, _("Deletion")), - (RECOVERY, _("Deletion recovery")), + (CREATION, "Creation"), + (PUBLICATION, "Publication"), + (DELETION, "Deletion"), + (RECOVERY, "Deletion recovery"), ) diff --git a/contentcuration/contentcuration/constants/user_history.py b/contentcuration/contentcuration/constants/user_history.py new file mode 100644 index 0000000000..9adc9b56c6 --- /dev/null +++ b/contentcuration/contentcuration/constants/user_history.py @@ -0,0 +1,9 @@ +DELETION = "soft-deletion" +RECOVERY = "soft-recovery" +RELATED_DATA_HARD_DELETION = "related-data-hard-deletion" + +choices = ( + (DELETION, "User soft deletion"), + (RECOVERY, "User soft deletion recovery"), + (RELATED_DATA_HARD_DELETION, "User related data hard deletion"), +) diff --git a/contentcuration/contentcuration/db/models/manager.py b/contentcuration/contentcuration/db/models/manager.py index 72e15186a7..4d833caf8f 100644 --- a/contentcuration/contentcuration/db/models/manager.py +++ b/contentcuration/contentcuration/db/models/manager.py @@ -47,14 +47,33 @@ def log_lock_time_spent(timespent): logging.debug("Spent {} seconds inside an mptt lock".format(timespent)) -def execute_queryset_without_results(queryset): - query = queryset.query - compiler = query.get_compiler(queryset.db) - sql, params = compiler.as_sql() - if not sql: - return - cursor = compiler.connection.cursor() - cursor.execute(sql, params) +# Fields that are allowed to be overridden on copies coming from a source that the user +# does not have edit rights to. +ALLOWED_OVERRIDES = { + "node_id", + "title", + "description", + "aggregator", + "provider", + "language_id", + "grade_levels", + "resource_types", + "learning_activities", + "accessibility_labels", + "categories", + "learner_needs", + "role", + "extra_fields", + "suggested_duration", +} + +EDIT_ALLOWED_OVERRIDES = ALLOWED_OVERRIDES.union({ + "license_id", + "license_description", + "extra_fields", + "copyright_holder", + "author", +}) class CustomContentNodeTreeManager(TreeManager.from_queryset(CustomTreeQuerySet)): @@ -272,7 +291,10 @@ def _clone_node( copy.update(self.get_source_attributes(source)) if isinstance(mods, dict): - copy.update(mods) + allowed_keys = EDIT_ALLOWED_OVERRIDES if can_edit_source_channel else ALLOWED_OVERRIDES + for key, value in mods.items(): + if key in copy and key in allowed_keys: + copy[key] = value # There might be some legacy nodes that don't have these, so ensure they are added if ( diff --git a/contentcuration/contentcuration/debug/middleware.py b/contentcuration/contentcuration/debug/middleware.py deleted file mode 100644 index 803a94f89b..0000000000 --- a/contentcuration/contentcuration/debug/middleware.py +++ /dev/null @@ -1,47 +0,0 @@ -import threading -import time - -import debug_panel.urls -from debug_panel.cache import cache -from debug_panel.middleware import DebugPanelMiddleware -from django.urls import reverse - - -class CustomDebugPanelMiddleware(DebugPanelMiddleware): - """ - Custom version to fix SQL escaping: - https://github.com/recamshak/django-debug-panel/issues/17#issuecomment-366268893 - """ - - def process_response(self, request, response): - """ - Store the DebugToolbarMiddleware rendered toolbar into a cache store. - The data stored in the cache are then reachable from an URL that is appened - to the HTTP response header under the 'X-debug-data-url' key. - """ - toolbar = self.__class__.debug_toolbars.get( - threading.current_thread().ident, None - ) - - response = super(DebugPanelMiddleware, self).process_response(request, response) - - if toolbar: - # for django-debug-toolbar >= 1.4 - for panel in reversed(toolbar.enabled_panels): - if ( - hasattr(panel, "generate_stats") and not panel.get_stats() - ): # PATCH HERE - panel.generate_stats(request, response) - - cache_key = "%f" % time.time() - cache.set(cache_key, toolbar.render_toolbar()) - - response["X-debug-data-url"] = request.build_absolute_uri( - reverse( - "debug_data", - urlconf=debug_panel.urls, - kwargs={"cache_key": cache_key}, - ) - ) - - return response diff --git a/contentcuration/contentcuration/debug_panel_settings.py b/contentcuration/contentcuration/debug_panel_settings.py index 79f9ddac6e..c097acbbc6 100644 --- a/contentcuration/contentcuration/debug_panel_settings.py +++ b/contentcuration/contentcuration/debug_panel_settings.py @@ -1,8 +1,13 @@ from .dev_settings import * # noqa -# These endpoints will throw an error on the django debug panel +# These endpoints will throw an error on the django debug panel. EXCLUDED_DEBUG_URLS = [ "/content/storage", + + # Disabling sync API because as soon as the sync API gets polled + # the current request data gets overwritten. + # Can be removed after websockets deployment. + "/api/sync", ] DEBUG_PANEL_ACTIVE = True @@ -14,10 +19,10 @@ def custom_show_toolbar(request): ) # noqa F405 -# if debug_panel exists, add it to our INSTALLED_APPS +# if debug_panel exists, add it to our INSTALLED_APPS. INSTALLED_APPS += ("debug_panel", "debug_toolbar", "pympler") # noqa F405 MIDDLEWARE += ( # noqa F405 - "contentcuration.debug.middleware.CustomDebugPanelMiddleware", + "debug_toolbar.middleware.DebugToolbarMiddleware", ) DEBUG_TOOLBAR_CONFIG = { "SHOW_TOOLBAR_CALLBACK": custom_show_toolbar, diff --git a/contentcuration/contentcuration/forms.py b/contentcuration/contentcuration/forms.py index 973916431e..d9dc781f61 100644 --- a/contentcuration/contentcuration/forms.py +++ b/contentcuration/contentcuration/forms.py @@ -7,6 +7,7 @@ from django.contrib.auth.forms import UserChangeForm from django.contrib.auth.forms import UserCreationForm from django.core import signing +from django.db.models import Q from django.template.loader import render_to_string from contentcuration.models import User @@ -45,7 +46,7 @@ class RegistrationForm(UserCreationForm, ExtraFormMixin): def clean_email(self): email = self.cleaned_data['email'].strip().lower() - if User.objects.filter(email__iexact=email, is_active=True).exists(): + if User.objects.filter(Q(is_active=True) | Q(deleted=True), email__iexact=email).exists(): raise UserWarning return email diff --git a/contentcuration/contentcuration/frontend/accounts/components/MessageLayout.vue b/contentcuration/contentcuration/frontend/accounts/components/MessageLayout.vue index 0a89c4ba5d..869ccf3a88 100644 --- a/contentcuration/contentcuration/frontend/accounts/components/MessageLayout.vue +++ b/contentcuration/contentcuration/frontend/accounts/components/MessageLayout.vue @@ -13,7 +13,10 @@

- +

diff --git a/contentcuration/contentcuration/frontend/accounts/pages/Create.vue b/contentcuration/contentcuration/frontend/accounts/pages/Create.vue index 361ffce01d..b211cd5634 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/Create.vue +++ b/contentcuration/contentcuration/frontend/accounts/pages/Create.vue @@ -15,7 +15,7 @@

{{ $tr('createAnAccountTitle') }}

- + {{ registrationFailed ? $tr('registrationFailed') : $tr('errorsMessage') }} @@ -131,42 +131,45 @@ /> - - + +
+ + - - - + + | + + + +
+ +
+
+ {{ $tr('contactMessage') }} +
+
-

- {{ $tr('contactMessage') }} -

- - {{ $tr('finishButton') }} - +
@@ -238,12 +241,19 @@ passwordConfirmRules() { return [value => (this.form.password1 === value ? true : this.$tr('passwordMatchMessage'))]; }, - tosRules() { + tosAndPolicyRules() { return [value => (value ? true : this.$tr('ToSRequiredMessage'))]; }, - policyRules() { - return [value => (value ? true : this.$tr('privacyPolicyRequiredMessage'))]; + acceptedAgreement: { + get() { + return this.form.accepted_tos && this.form.accepted_policy; + }, + set(accepted) { + this.form.accepted_tos = accepted; + this.form.accepted_policy = accepted; + }, }, + usageOptions() { return [ { @@ -350,7 +360,7 @@ }, clean() { return data => { - let cleanedData = { ...data, policies: {} }; + const cleanedData = { ...data, policies: {} }; Object.keys(cleanedData).forEach(key => { // Trim text fields if (key === 'source') { @@ -413,10 +423,9 @@ showOtherField(id) { return id === uses.OTHER && this.form.uses.includes(id); }, - submit() { if (this.$refs.form.validate()) { - let cleanedData = this.clean(this.form); + const cleanedData = this.clean(this.form); return this.register(cleanedData) .then(() => { this.$router.push({ name: 'ActivationSent' }); @@ -439,6 +448,7 @@ return Promise.resolve(); }, }, + $trs: { backToLoginButton: 'Sign in', createAnAccountTitle: 'Create an account', @@ -447,7 +457,6 @@ registrationFailed: 'There was an error registering your account. Please try again', registrationFailedOffline: 'You seem to be offline. Please connect to the internet to create an account.', - // Basic information strings basicInformationHeader: 'Basic information', firstNameLabel: 'First name', @@ -492,15 +501,13 @@ otherSourcePlaceholder: 'Please describe', // Privacy policy + terms of service - viewToSLink: 'View terms of service', - ToSCheck: 'I have read and agree to the terms of service', - ToSRequiredMessage: 'Please accept our terms of service', + viewToSLink: 'View Terms of Service', + ToSRequiredMessage: 'Please accept our terms of service and policy', - viewPrivacyPolicyLink: 'View privacy policy', - privacyPolicyCheck: 'I have read and agree to the privacy policy', - privacyPolicyRequiredMessage: 'Please accept our privacy policy', + viewPrivacyPolicyLink: 'View Privacy Policy', contactMessage: 'Questions or concerns? Please email us at content@learningequality.org', finishButton: 'Finish', + agreement: 'I have read and agree to terms of service and the privacy policy', }, }; @@ -521,6 +528,11 @@ } } + .policy-checkbox /deep/ .v-messages { + min-height: 0; + margin-left: 40px; + } + iframe { width: 100%; min-height: 400px; @@ -529,4 +541,23 @@ border: 0; } + .span-spacing { + display: flex; + margin-left: 40px; + } + + .span-spacing span { + margin-left: 2px; + font-size: 16px; + } + + .span-spacing-email { + margin-left: 3px; + font-size: 16px; + } + + .align-items { + display: block; + } + diff --git a/contentcuration/contentcuration/frontend/accounts/pages/Main.vue b/contentcuration/contentcuration/frontend/accounts/pages/Main.vue index c15a96fca1..49833f60e2 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/Main.vue +++ b/contentcuration/contentcuration/frontend/accounts/pages/Main.vue @@ -6,12 +6,12 @@ justify-center class="main pt-5" > -

- -

- + {{ $tr('kolibriStudio') }} - - + + - - - + + +

- +

- - {{ $tr('signInButton') }} - - - {{ $tr('createAccountButton') }} - + +

- +

@@ -90,7 +123,6 @@ import PolicyModals from 'shared/views/policies/PolicyModals'; import { policies } from 'shared/constants'; import LanguageSwitcherList from 'shared/languageSwitcher/LanguageSwitcherList'; - import OfflineText from 'shared/views/OfflineText'; export default { name: 'Main', @@ -100,7 +132,6 @@ LanguageSwitcherList, PasswordField, PolicyModals, - OfflineText, }, data() { return { @@ -132,7 +163,7 @@ submit() { if (this.$refs.form.validate()) { this.busy = true; - let credentials = { + const credentials = { username: this.username, password: this.password, }; @@ -180,6 +211,7 @@ .main { overflow: auto; + /* stylelint-disable-next-line custom-property-pattern */ background-color: var(--v-backgroundColor-base); } @@ -191,10 +223,8 @@ content: '•'; } - .corner { - position: absolute; - top: 1em; - left: 1em; + .w-100 { + width: 100%; } diff --git a/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js b/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js index dc9f24df06..f2a201b560 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js +++ b/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js @@ -34,7 +34,7 @@ const defaultData = { const register = jest.fn(); function makeWrapper(formData) { - let wrapper = mount(Create, { + const wrapper = mount(Create, { router, computed: { getPolicyAcceptedData() { @@ -62,7 +62,6 @@ function makeWrapper(formData) { }); return wrapper; } - function makeFailedPromise(statusCode) { return () => { return new Promise((resolve, reject) => { @@ -81,13 +80,13 @@ describe('create', () => { }); it('should trigger submit method when form is submitted', () => { const submit = jest.fn(); - let wrapper = makeWrapper(); + const wrapper = makeWrapper(); wrapper.setMethods({ submit }); wrapper.find({ ref: 'form' }).trigger('submit'); expect(submit).toHaveBeenCalled(); }); it('should call register with form data', () => { - let wrapper = makeWrapper(); + const wrapper = makeWrapper(); wrapper.find({ ref: 'form' }).trigger('submit'); expect(register.mock.calls[0][0]).toEqual({ ...defaultData, @@ -98,12 +97,12 @@ describe('create', () => { }); it('should automatically fill the email if provided in the query param', () => { router.push({ name: 'Create', query: { email: 'newtest@test.com' } }); - let wrapper = mount(Create, { router, stubs: ['PolicyModals'], mocks: connectionStateMocks }); + const wrapper = mount(Create, { router, stubs: ['PolicyModals'], mocks: connectionStateMocks }); expect(wrapper.vm.form.email).toBe('newtest@test.com'); }); describe('validation', () => { it('should call register if form is valid', () => { - let wrapper = makeWrapper(); + const wrapper = makeWrapper(); wrapper.vm.submit(); expect(register).toHaveBeenCalled(); }); @@ -122,26 +121,26 @@ describe('create', () => { }; Object.keys(form).forEach(field => { - let wrapper = makeWrapper({ [field]: form[field] }); + const wrapper = makeWrapper({ [field]: form[field] }); wrapper.vm.submit(); expect(register).not.toHaveBeenCalled(); }); }); it('should fail if password1 and password2 do not match', () => { - let wrapper = makeWrapper({ password1: 'some other password' }); + const wrapper = makeWrapper({ password1: 'some other password' }); wrapper.vm.submit(); expect(register).not.toHaveBeenCalled(); }); it('should fail if uses field is set to fields that require more input that is not provided', () => { [uses.STORING, uses.OTHER].forEach(use => { - let wrapper = makeWrapper({ uses: [use] }); + const wrapper = makeWrapper({ uses: [use] }); wrapper.vm.submit(); expect(register).not.toHaveBeenCalled(); }); }); it('should fail if source field is set to an option that requires more input that is not provided', () => { [sources.ORGANIZATION, sources.CONFERENCE, sources.OTHER].forEach(source => { - let wrapper = makeWrapper({ source }); + const wrapper = makeWrapper({ source }); wrapper.vm.submit(); expect(register).not.toHaveBeenCalled(); }); diff --git a/contentcuration/contentcuration/frontend/accounts/pages/__tests__/main.spec.js b/contentcuration/contentcuration/frontend/accounts/pages/__tests__/main.spec.js index dc57ccf210..2525a83d57 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/__tests__/main.spec.js +++ b/contentcuration/contentcuration/frontend/accounts/pages/__tests__/main.spec.js @@ -5,7 +5,7 @@ import Main from '../Main'; const login = jest.fn(); function makeWrapper() { - let wrapper = mount(Main, { + const wrapper = mount(Main, { router, stubs: ['GlobalSnackbar', 'PolicyModals'], mocks: { diff --git a/contentcuration/contentcuration/frontend/accounts/pages/accountDeleted/AccountDeleted.vue b/contentcuration/contentcuration/frontend/accounts/pages/accountDeleted/AccountDeleted.vue index f881737065..032cfc506a 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/accountDeleted/AccountDeleted.vue +++ b/contentcuration/contentcuration/frontend/accounts/pages/accountDeleted/AccountDeleted.vue @@ -5,7 +5,7 @@ > @@ -24,7 +24,7 @@ }, $trs: { accountDeletedTitle: 'Account successfully deleted', - continueToSignIn: 'Continue to sign-in page', + backToLogin: 'Continue to sign-in page', }, }; diff --git a/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/AccountCreated.vue b/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/AccountCreated.vue index b55bbdf418..14e106c232 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/AccountCreated.vue +++ b/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/AccountCreated.vue @@ -5,7 +5,7 @@ > @@ -24,7 +24,7 @@ }, $trs: { accountCreatedTitle: 'Account successfully created', - continueToSignIn: 'Continue to sign-in', + backToLogin: 'Continue to sign-in page', }, }; diff --git a/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/AccountNotActivated.vue b/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/AccountNotActivated.vue index a0db43bca2..ccbec003d1 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/AccountNotActivated.vue +++ b/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/AccountNotActivated.vue @@ -4,9 +4,12 @@ :header="$tr('title')" :text="$tr('text')" > - - {{ $tr('requestNewLink') }} - + diff --git a/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/ActivationExpired.vue b/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/ActivationExpired.vue index cdf2db7a43..9dd1dad18c 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/ActivationExpired.vue +++ b/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/ActivationExpired.vue @@ -4,9 +4,12 @@ :header="$tr('activationExpiredTitle')" :text="$tr('activationExpiredText')" > - - {{ $tr('requestNewLink') }} - + diff --git a/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/RequestNewActivationLink.vue b/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/RequestNewActivationLink.vue index df889b3ecb..afb06cdaca 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/RequestNewActivationLink.vue +++ b/contentcuration/contentcuration/frontend/accounts/pages/activateAccount/RequestNewActivationLink.vue @@ -11,9 +11,12 @@ > - - {{ $tr('submitButton') }} - + @@ -64,3 +67,11 @@ }; + + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/accounts/pages/resetPassword/ForgotPassword.vue b/contentcuration/contentcuration/frontend/accounts/pages/resetPassword/ForgotPassword.vue index 13910a9bad..7349cf97ff 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/resetPassword/ForgotPassword.vue +++ b/contentcuration/contentcuration/frontend/accounts/pages/resetPassword/ForgotPassword.vue @@ -7,9 +7,12 @@ - - {{ $tr('submitButton') }} - + @@ -65,3 +68,11 @@ }; + + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/accounts/pages/resetPassword/ResetLinkExpired.vue b/contentcuration/contentcuration/frontend/accounts/pages/resetPassword/ResetLinkExpired.vue index 4f0c8fc057..0249fd9fda 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/resetPassword/ResetLinkExpired.vue +++ b/contentcuration/contentcuration/frontend/accounts/pages/resetPassword/ResetLinkExpired.vue @@ -4,9 +4,12 @@ :header="$tr('resetExpiredTitle')" :text="$tr('resetExpiredText')" > - - {{ $tr('requestNewLink') }} - + diff --git a/contentcuration/contentcuration/frontend/accounts/pages/resetPassword/ResetPassword.vue b/contentcuration/contentcuration/frontend/accounts/pages/resetPassword/ResetPassword.vue index b3866f5ccd..2fd3ceddd3 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/resetPassword/ResetPassword.vue +++ b/contentcuration/contentcuration/frontend/accounts/pages/resetPassword/ResetPassword.vue @@ -16,9 +16,12 @@ :label="$tr('passwordConfirmLabel')" :additionalRules="passwordConfirmRules" /> - - {{ $tr('submitButton') }} - + @@ -55,7 +58,7 @@ submit() { this.error = false; if (this.$refs.form.validate()) { - let payload = { + const payload = { ...this.$route.query, new_password1: this.new_password1, new_password2: this.new_password2, @@ -84,3 +87,11 @@ }; + + diff --git a/contentcuration/contentcuration/frontend/accounts/vuex/index.js b/contentcuration/contentcuration/frontend/accounts/vuex/index.js index bc95d1ed29..f64df2778a 100644 --- a/contentcuration/contentcuration/frontend/accounts/vuex/index.js +++ b/contentcuration/contentcuration/frontend/accounts/vuex/index.js @@ -17,7 +17,7 @@ export default { return client.post(window.Urls.auth_password_reset(), { email }); }, setPassword(context, { uidb64, token, new_password1, new_password2 }) { - let data = { + const data = { new_password1, new_password2, }; diff --git a/contentcuration/contentcuration/frontend/administration/mixins.js b/contentcuration/contentcuration/frontend/administration/mixins.js index 529c75ffae..262dbf3cc9 100644 --- a/contentcuration/contentcuration/frontend/administration/mixins.js +++ b/contentcuration/contentcuration/frontend/administration/mixins.js @@ -24,7 +24,7 @@ export function generateFilterMixin(filterMap) { return this.$route.query.keywords; }, set(value) { - let params = { ...this.$route.query, page: 1 }; + const params = { ...this.$route.query, page: 1 }; if (value) { params.keywords = value; } else { @@ -37,7 +37,7 @@ export function generateFilterMixin(filterMap) { get() { // Return filter where all param conditions are met const filterKeys = intersection(Object.keys(this.$route.query), paramKeys); - let key = findKey(filterMap, value => { + const key = findKey(filterMap, value => { return filterKeys.every(field => { return value.params[field] === _getBooleanVal(this.$route.query[field]); }); @@ -115,7 +115,7 @@ export const tableMixin = { computed: { pagination: { get() { - let params = { + const params = { rowsPerPage: Number(this.$route.query.page_size) || 25, page: Number(this.$route.query.page) || 1, }; diff --git a/contentcuration/contentcuration/frontend/administration/pages/AdministrationIndex.vue b/contentcuration/contentcuration/frontend/administration/pages/AdministrationIndex.vue index 7ed794230f..10f2b67efb 100644 --- a/contentcuration/contentcuration/frontend/administration/pages/AdministrationIndex.vue +++ b/contentcuration/contentcuration/frontend/administration/pages/AdministrationIndex.vue @@ -128,6 +128,7 @@ .v-icon:not(.v-icon--is-component) { font-size: 16pt !important; + /* stylelint-disable-next-line custom-property-pattern */ color: var(--v-darkGrey-darken1) !important; opacity: 1 !important; transform: none !important; @@ -159,6 +160,7 @@ } tr:hover td { + /* stylelint-disable-next-line custom-property-pattern */ background-color: var(--v-greyBackground-base) !important; } diff --git a/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelActionsDropdown.vue b/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelActionsDropdown.vue index d71841492f..e243a8fd98 100644 --- a/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelActionsDropdown.vue +++ b/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelActionsDropdown.vue @@ -146,14 +146,11 @@ channel() { return this.getChannel(this.channelId); }, - name() { - return this.channel.name; - }, searchChannelEditorsLink() { return { name: RouteNames.USERS, query: { - keywords: `${this.name} ${this.channel.id}`, + keywords: `${this.channel.id}`, }, }; }, diff --git a/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelItem.vue b/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelItem.vue index 3e975714af..d87e1d4d3f 100644 --- a/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelItem.vue +++ b/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelItem.vue @@ -176,8 +176,8 @@ import ClipboardChip from '../../components/ClipboardChip'; import { RouteNames } from '../../constants'; import ChannelActionsDropdown from './ChannelActionsDropdown'; - import Checkbox from 'shared/views/form/Checkbox'; import { fileSizeMixin } from 'shared/mixins'; + import Checkbox from 'shared/views/form/Checkbox'; export default { name: 'ChannelItem', @@ -232,7 +232,7 @@ return { name: RouteNames.USERS, query: { - keywords: `${this.channel.name} ${this.channelId}`, + keywords: `${this.channelId}`, }, }; }, diff --git a/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelTable.vue b/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelTable.vue index 76bfc4d4e2..b4185d8a85 100644 --- a/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelTable.vue +++ b/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelTable.vue @@ -100,8 +100,8 @@ diff --git a/contentcuration/contentcuration/frontend/channelEdit/views/progress/ProgressModal.vue b/contentcuration/contentcuration/frontend/channelEdit/views/progress/ProgressModal.vue index f5ce76de6b..a124e0cc6e 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/views/progress/ProgressModal.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/views/progress/ProgressModal.vue @@ -1,14 +1,21 @@