Skip to content

Commit

Permalink
Merge pull request #11371 from learningequality/release-v0.16.x
Browse files Browse the repository at this point in the history
Merge release-v0.16.x into develop
  • Loading branch information
rtibbles authored Oct 8, 2023
2 parents fa3f140 + 23a5b6e commit 3435a54
Show file tree
Hide file tree
Showing 45 changed files with 692 additions and 271 deletions.
7 changes: 3 additions & 4 deletions integration_testing/scripts/run_kolibri_app_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ def SERVING(self, port):
self.port = port

def RUN(self):
start_url = (
"http://127.0.0.1:{port}".format(port=self.port)
+ interface.get_initialize_url()
)
start_url = "http://127.0.0.1:{port}".format(
port=self.port
) + interface.get_initialize_url(auth_token="1234")
print("Kolibri running at: {start_url}".format(start_url=start_url))


Expand Down
2 changes: 2 additions & 0 deletions kolibri/core/assets/src/core-app/apiSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import UiAlert from 'kolibri-design-system/lib/keen/UiAlert';
import responsiveWindowMixin from 'kolibri-design-system/lib/KResponsiveWindowMixin';
import responsiveElementMixin from 'kolibri-design-system/lib/KResponsiveElementMixin';
import useKResponsiveWindow from 'kolibri-design-system/lib/useKResponsiveWindow';
import useKShow from 'kolibri-design-system/lib/composables/useKShow';
import UiIconButton from 'kolibri-design-system/lib/keen/UiIconButton'; // temp hack
import * as vueCompositionApi from '@vue/composition-api';
import logging from '../logging';
Expand Down Expand Up @@ -228,6 +229,7 @@ export default {
},
composables: {
useKResponsiveWindow,
useKShow,
useMinimumKolibriVersion,
useUser,
},
Expand Down
7 changes: 2 additions & 5 deletions kolibri/core/assets/src/views/sortable/DragContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,17 @@
}
@keyframes bounce-in {
from {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
transform: scale3d(1.05, 1.05, 1.05);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
50% {
transform: scale3d(0.98, 0.98, 0.98);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
to {
100% {
transform: scale3d(1, 1, 1);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,12 @@
methods: {
makeYearOptions(max, min) {
return range(max, min, -1).map(n => {
// Because of timezone, year could be mismatched when localized in any
// timezone that less than UTC. for ex- 2022 will be shown instead of 2023
const date = new Date();
date.setFullYear(n);
return {
label: this.$formatDate(String(n), { year: 'numeric' }),
label: this.$formatDate(String(date), { year: 'numeric' }),
value: String(n),
};
});
Expand Down
5 changes: 2 additions & 3 deletions kolibri/core/auth/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from kolibri.core.tasks.exceptions import JobNotFound
from kolibri.core.tasks.exceptions import UserCancelledError
from kolibri.core.tasks.job import JobStatus
from kolibri.core.tasks.job import Priority
from kolibri.core.tasks.job import State
from kolibri.core.tasks.main import job_storage
from kolibri.core.tasks.permissions import IsAdminForJob
Expand Down Expand Up @@ -627,9 +628,7 @@ def stop_request_soud_sync(server, user):
stoppeerusersync(server, user)


@register_task(
queue=soud_sync_queue,
)
@register_task(queue=soud_sync_queue, priority=Priority.HIGH, status_fn=status_fn)
def request_soud_sync(server, user, queue_id=None, ttl=4):
"""
Make a request to the serverurl endpoint to sync this SoUD (Subset of Users Device)
Expand Down
52 changes: 52 additions & 0 deletions kolibri/core/auth/test/test_morango_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ def test_scenarios(self, servers):
s1.create_model(Role, collection_id=fac.id, user_id=alk_user.id, kind="admin")
s0.create_model(Role, collection_id=fac.id, user_id=alk_user.id, kind="admin")
s1.sync(s0, facility)
s2.sync(s0, facility)
role = Role.objects.using(s1.db_alias).get(user=alk_user)
admin_role = Store.objects.using(s1.db_alias).get(id=role.id)
self.assertTrue(admin_role.conflicting_serialized_data)
Expand All @@ -397,6 +398,41 @@ def test_scenarios(self, servers):
)
self.assertTrue(Store.objects.using(s1.db_alias).get(id=alk_user.id).deleted)

# assert deleted object is transitively propagated
s2.sync(s1, facility)
self.assertFalse(
FacilityUser.objects.using(s2.db_alias)
.filter(username="Antemblowind")
.exists()
)
self.assertTrue(Store.objects.using(s2.db_alias).get(id=alk_user.id).deleted)

# assert deletion takes priority over update
alk_user = FacilityUser.objects.using(s0.db_alias).create(
username="Antemblowind", facility=facility
)
# Sync to both devices
s1.sync(s0, facility)
s2.sync(s0, facility)

# delete on s0
s0.delete_model(FacilityUser, id=alk_user.id)
# update on s1
s1.update_model(FacilityUser, alk_user.id, username="Antemblowind2")
# Sync deletion to s2
s2.sync(s0, facility)
self.assertFalse(
FacilityUser.objects.using(s2.db_alias).filter(id=alk_user.id).exists()
)
# Sync update to s2
s2.sync(s1, facility)
self.assertFalse(
FacilityUser.objects.using(s2.db_alias).filter(id=alk_user.id).exists()
)
self.assertFalse(
FacilityUser.objects.using(s1.db_alias).filter(id=alk_user.id).exists()
)

# # role deletion and re-creation
# Change roles for users
alto_user = FacilityUser.objects.using(s1.db_alias).get(
Expand Down Expand Up @@ -847,6 +883,22 @@ def test_single_user_assignment_sync(self, servers):
self.assert_existence(
self.tablet, kind, assignment_id, should_exist=False
)
if disable_assignment == self.deactivate:
# If we're deactivating the assignment, try reactivating it to check that we
# can make it active again
self.set_active_state(self.laptop_a, kind, assignment_id, True)
self.sync_full_facility_servers()
self.assert_existence(
self.tablet, kind, assignment_id, should_exist=False
)
self.assert_existence(
self.laptop_b, kind, assignment_id, should_exist=True
)
self.sync_single_user(self.laptop_a)
# import IPython; IPython.embed()
self.assert_existence(
self.tablet, kind, assignment_id, should_exist=True
)

# Create exam on Laptop A, single-user sync to tablet, then modify exam on Laptop A and
# single-user sync again to check that "updating" works
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class ContentContentnode(Base):
learner_needs_bitmask_0 = Column(BigInteger)
learning_activities_bitmask_0 = Column(BigInteger)
ancestors = Column(Text)
admin_imported = Column(Boolean)
parent_id = Column(ForeignKey("content_contentnode.id"), index=True)

lang = relationship("ContentLanguage")
Expand Down
24 changes: 18 additions & 6 deletions kolibri/core/content/management/commands/deletecontent.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
logger = logging.getLogger(__name__)


def delete_metadata(channel, node_ids, exclude_node_ids, force_delete):
def delete_metadata(
channel, node_ids, exclude_node_ids, force_delete, ignore_admin_flags
):
# Only delete all metadata if we are not doing selective deletion
delete_all_metadata = not (node_ids or exclude_node_ids)

Expand All @@ -31,7 +33,9 @@ def delete_metadata(channel, node_ids, exclude_node_ids, force_delete):
)

# If we have been passed node ids do not do a full deletion pass
set_content_invisible(channel.id, node_ids, exclude_node_ids)
set_content_invisible(
channel.id, node_ids, exclude_node_ids, not ignore_admin_flags
)
# If everything has been made invisible, delete all the metadata
delete_all_metadata = delete_all_metadata or not channel.root.available

Expand Down Expand Up @@ -123,11 +127,20 @@ def add_arguments(self, parser):
help="Ensure removal of files",
)

parser.add_argument(
"--ignore_admin_flags",
action="store_false",
dest="ignore_admin_flags",
default=True,
help="Don't modify admin_imported values when deleting content",
)

def handle_async(self, *args, **options):
channel_id = options["channel_id"]
node_ids = options["node_ids"]
exclude_node_ids = options["exclude_node_ids"]
force_delete = options["force_delete"]
ignore_admin_flags = options["ignore_admin_flags"]

try:
channel = ChannelMetadata.objects.get(pk=channel_id)
Expand All @@ -136,10 +149,9 @@ def handle_async(self, *args, **options):
"Channel matching id {id} does not exist".format(id=channel_id)
)

(
total_resource_number,
delete_all_metadata,
) = delete_metadata(channel, node_ids, exclude_node_ids, force_delete)
(total_resource_number, delete_all_metadata,) = delete_metadata(
channel, node_ids, exclude_node_ids, force_delete, ignore_admin_flags
)
unused_files = LocalFile.objects.get_unused_files()
# Get the number of files that are being deleted
unused_files_count = unused_files.count()
Expand Down
4 changes: 2 additions & 2 deletions kolibri/core/content/management/commands/generate_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from kolibri.core.content.apps import KolibriContentConfig
from kolibri.core.content.constants.schema_versions import CONTENT_SCHEMA_VERSION
from kolibri.core.content.constants.schema_versions import CURRENT_SCHEMA_VERSION
from kolibri.core.content.utils.channel_import import models_to_exclude
from kolibri.core.content.utils.channel_import import no_schema_models
from kolibri.core.content.utils.sqlalchemybridge import __SQLALCHEMY_CLASSES_MODULE_NAME
from kolibri.core.content.utils.sqlalchemybridge import __SQLALCHEMY_CLASSES_PATH
from kolibri.core.content.utils.sqlalchemybridge import (
Expand Down Expand Up @@ -100,7 +100,7 @@ def handle(self, *args, **options):
table_names = [
model._meta.db_table
for name, model in app_config.models.items()
if name != "channelmetadatacache" and model not in models_to_exclude
if name != "channelmetadatacache" and model not in no_schema_models
]
metadata.reflect(bind=engine, only=table_names)
Base = prepare_base(metadata, name=version)
Expand Down
33 changes: 33 additions & 0 deletions kolibri/core/content/management/commands/importchannel.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,13 @@ def progress_callback(bytes):
# if upgrading, import the channel
if not no_upgrade:
try:
# In each case we need to evaluate the queryset now,
# in order to get node ids as they currently are before
# the import. If we did not coerce each of these querysets
# to a list now, they would be lazily evaluated after the
# import, and would reflect the state of the database
# after the import.

# evaluate list so we have the current node ids
node_ids = list(
ContentNode.objects.filter(
Expand All @@ -214,13 +221,39 @@ def progress_callback(bytes):
.exclude(kind=content_kinds.TOPIC)
.values_list("id", flat=True)
)
# evaluate list so we have the current node ids
admin_imported_ids = list(
ContentNode.objects.filter(
channel_id=channel_id, available=True, admin_imported=True
)
.exclude(kind=content_kinds.TOPIC)
.values_list("id", flat=True)
)
# evaluate list so we have the current node ids
not_admin_imported_ids = list(
ContentNode.objects.filter(
channel_id=channel_id, available=True, admin_imported=False
)
.exclude(kind=content_kinds.TOPIC)
.values_list("id", flat=True)
)
import_ran = import_channel_by_id(
channel_id, self.is_cancelled, contentfolder
)
if import_ran:
if node_ids:
# annotate default channel db based on previously annotated leaf nodes
update_content_metadata(channel_id, node_ids=node_ids)
if admin_imported_ids:
# Reset admin_imported flag for nodes that were imported by admin
ContentNode.objects.filter_by_uuids(
admin_imported_ids
).update(admin_imported=True)
if not_admin_imported_ids:
# Reset admin_imported flag for nodes that were not imported by admin
ContentNode.objects.filter_by_uuids(
not_admin_imported_ids
).update(admin_imported=False)
else:
# ensure the channel is available to the frontend
ContentCacheKey.update_cache_key()
Expand Down
Loading

0 comments on commit 3435a54

Please sign in to comment.