diff --git a/.env b/.env index 8ffb031f..26863cf9 100644 --- a/.env +++ b/.env @@ -3,7 +3,7 @@ # you wish to override # Set the edx-platform named release. The default release is "hawthorn" -OPENEDX_RELEASE=juniper +OPENEDX_RELEASE=maple # Env settings TODO # - PyPI authentication for Twine diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 44494439..adc04be4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,7 +3,6 @@ name: figures tests on: push: branches: - - main - develop/maple pull_request: @@ -13,16 +12,10 @@ jobs: strategy: matrix: include: - - python: 2.7 - tox-env: py27-ginkgo - - python: 2.7 - tox-env: py27-hawthorn - - python: 2.7 - tox-env: py27-hawthorn_multisite - - python: 3.5 - tox-env: py35-juniper_community - - python: 3.5 - tox-env: py35-juniper_multisite + - python: 3.8 + tox-env: py38-maple_community + - python: 3.8 + tox-env: py35-maple_multisite - python: 3.8 tox-env: lint - python: 3.8 diff --git a/.gitignore b/.gitignore index a372036f..465bd33d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +figures/static/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -108,6 +110,9 @@ node_modules # PyCharm .idea +# VScode +.vscode + frontend/webpack-stats.json # Sublime Text diff --git a/DEVELOPER-QUICKSTART.md b/DEVELOPER-QUICKSTART.md index 6b1eec44..f61d1ced 100644 --- a/DEVELOPER-QUICKSTART.md +++ b/DEVELOPER-QUICKSTART.md @@ -1,4 +1,3 @@ - # Figures Developer Quickstart Guide ## 1. Overview @@ -15,9 +14,9 @@ You should use a Python virtualenv to run Figures standalone development mode. T Here are links to help guide setting up virtualenv -* https://virtualenv.pypa.io/en/stable/installation/ -* https://virtualenv.pypa.io/en/stable/userguide/ -* https://docs.python-guide.org/dev/virtualenvs/#lower-level-virtualenv +- https://virtualenv.pypa.io/en/stable/installation/ +- https://virtualenv.pypa.io/en/stable/userguide/ +- https://docs.python-guide.org/dev/virtualenvs/#lower-level-virtualenv ## 3. Clone Figures, install dependencies, and test @@ -25,19 +24,18 @@ On your development machine, open a terminal (command line shell) and create or **These instructions assume you are running a Python virtualenv** - In the terminal, run the following: ``` git clone https://github.com/appsembler/figures.git cd figures -pip install -r devsite/requirements/py35_juniper.txt +pip install -r devsite/requirements/community.txt ``` now we'll run [pytest](https://docs.pytest.org/) to make sure that the tests pass: ``` -OPENEDX_RELEASE=JUNIPER pytest -c pytest-juniper.ini +pytest ``` ## 4. Build front end assets @@ -46,7 +44,6 @@ Figures front end assets need to be build from the front end sources. Make sure Navigate to the `figures/frontend` directory. - Then download JavaScript dependencies: ``` @@ -110,10 +107,10 @@ _Section incomplete_ Pages of interest: -* http://127.0.0.1:8000/admin/ -* http://127.0.0.1:8000/figures/ -* http://127.0.0.1:8000/figures/api/ -* http://127.0.0.1:8000/admin/figures/ +- http://127.0.0.1:8000/admin/ +- http://127.0.0.1:8000/figures/ +- http://127.0.0.1:8000/figures/api/ +- http://127.0.0.1:8000/admin/figures/ ## 8. Running the pipeline manually @@ -129,7 +126,7 @@ Make sure you have your development virtualenv running. Then navigate to the `de This will run the pipeline immediately instead of being queued to celery. -*NOTE* The above is just for running the pipeline in Figures standalone mode with the included devsite. If you are working with Figures in Open edX (either devstack or fullstack), run the following: +_NOTE_ The above is just for running the pipeline in Figures standalone mode with the included devsite. If you are working with Figures in Open edX (either devstack or fullstack), run the following: ``` ./manage.py lms populate_figures_metrics --no-delay @@ -143,7 +140,6 @@ Go to the `frontend` directory and run `yarn`. This will install dependencies Run `yarn build` to compile the front end assets. - ## 10. Running the frontend Webpack server _TODO: Add this section_ diff --git a/devsite/devsite/cans/users.py b/devsite/devsite/cans/users.py index 507a8419..fa7ad9de 100644 --- a/devsite/devsite/cans/users.py +++ b/devsite/devsite/cans/users.py @@ -7,7 +7,7 @@ import random import faker -from student.models import UserProfile +from common.djangoapps.student.models import UserProfile class UserGenerator(object): diff --git a/devsite/devsite/celery.py b/devsite/devsite/celery.py index 674330df..a4c1c3d1 100644 --- a/devsite/devsite/celery.py +++ b/devsite/devsite/celery.py @@ -8,13 +8,13 @@ from django.conf import settings -CELERY_CHECK_MSG_PREFIX = 'figures-devsite-celery-check' +CELERY_CHECK_MSG_PREFIX = "figures-devsite-celery-check" # set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'devsite.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "devsite.settings") -app = Celery('devsite') +app = Celery("devsite") # For Celery 4.0+ # @@ -25,21 +25,21 @@ # See: https://docs.celeryproject.org/en/4.0/whatsnew-4.0.html # `app.config_from_object('django.conf:settings', namespace='CELERY')` -app.config_from_object('django.conf:settings') +app.config_from_object("django.conf:settings") # Load task modules from all registered Django app configs. app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) - -app.conf.update( - CELERY_RESULT_BACKEND='djcelery.backends.database:DatabaseBackend', -) +# TODO update django-celery is not needed for celery 4.4.+ +# app.conf.update( +# CELERY_RESULT_BACKEND="django-db", +# ) @app.task(bind=True) def debug_task(self): - print(('Request: {0!r}'.format(self.request))) + print(("Request: {0!r}".format(self.request))) @app.task(bind=True) @@ -49,4 +49,4 @@ def celery_check(self, msg): Returns a value so that we can test Celery results backend configuration """ print(('Called devsite.celery.celery.check with message "{}"'.format(msg))) - return '{prefix}:{msg}'.format(prefix=CELERY_CHECK_MSG_PREFIX, msg=msg) + return "{prefix}:{msg}".format(prefix=CELERY_CHECK_MSG_PREFIX, msg=msg) diff --git a/devsite/devsite/seed.py b/devsite/devsite/seed.py index b466707e..371b64f6 100644 --- a/devsite/devsite/seed.py +++ b/devsite/devsite/seed.py @@ -21,7 +21,7 @@ from figures.compat import StudentModule from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from student.models import CourseAccessRole, CourseEnrollment, UserProfile +from common.djangoapps.student.models import CourseAccessRole, CourseEnrollment, UserProfile from organizations.models import Organization, OrganizationCourse diff --git a/devsite/devsite/settings.py b/devsite/devsite/settings.py index b10167fd..f9e05007 100644 --- a/devsite/devsite/settings.py +++ b/devsite/devsite/settings.py @@ -23,7 +23,7 @@ env = environ.Env( DEBUG=(bool, True), ALLOWED_HOSTS=(list, []), - OPENEDX_RELEASE=(str, 'JUNIPER'), + OPENEDX_RELEASE=(str, 'MAPLE'), FIGURES_IS_MULTISITE=(bool, False), ENABLE_DEVSITE_CELERY=(bool, True), ENABLE_OPENAPI_DOCS=(bool, False), @@ -35,12 +35,9 @@ OPENEDX_RELEASE = env('OPENEDX_RELEASE').upper() -if OPENEDX_RELEASE == 'GINKGO': - ENABLE_DEVSITE_CELERY = False -else: - ENABLE_DEVSITE_CELERY = env('ENABLE_DEVSITE_CELERY') +ENABLE_DEVSITE_CELERY = env('ENABLE_DEVSITE_CELERY') -MOCKS_DIR = 'mocks/{}'.format(OPENEDX_RELEASE.lower()) +MOCKS_DIR = 'mocks/' # SECURITY WARNING: Use a real key when running in the cloud and keep it secret SECRET_KEY = 'insecure-secret-key' @@ -84,51 +81,25 @@ # Also note the paths set in edx-figures/pytest.ini 'openedx.core.djangoapps.content.course_overviews', 'openedx.core.djangoapps.course_groups', - 'student', + 'lms.djangoapps.certificates', + 'lms.djangoapps.courseware', + 'common.djangoapps.student', ] if ENABLE_DEVSITE_CELERY: INSTALLED_APPS.append('djcelery') -if OPENEDX_RELEASE == 'GINKGO': - # certificates and courseware do NOT use the `lms.djangoapps.` namespace - # prefix on Ginkgo. See here: https://github.com/appsembler/figures/issues/433 - INSTALLED_APPS.append('certificates') - INSTALLED_APPS.append('courseware') -elif OPENEDX_RELEASE == 'HAWTHORN': - # Yes, this is correct, certificates was updated to uses the full namespace - # and courseware has not yet been updated - INSTALLED_APPS.append('lms.djangoapps.certificates') - INSTALLED_APPS.append('courseware') -else: - INSTALLED_APPS.append('lms.djangoapps.certificates') - INSTALLED_APPS.append('lms.djangoapps.courseware') - - -if OPENEDX_RELEASE == 'JUNIPER': - MIDDLEWARE = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'debug_toolbar.middleware.DebugToolbarMiddleware', - ) -else: - MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'debug_toolbar.middleware.DebugToolbarMiddleware', - ) +MIDDLEWARE = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'debug_toolbar.middleware.DebugToolbarMiddleware', +) ROOT_URLCONF = 'devsite.urls' @@ -183,7 +154,7 @@ } LOCALE_PATHS = [ - os.path.join(PROJECT_ROOT_DIR, 'figures', 'conf', 'locale') + os.path.join(PROJECT_ROOT_DIR, 'figures', 'conf', 'locale') ] @@ -243,7 +214,9 @@ # The LMS defines ``ENV_TOKENS`` to load settings declared in `lms.env.json` # We have an empty dict here to replicate behavior in the LMS -ENV_TOKENS = {} +ENV_TOKENS = { + 'FIGURES': {}, +} PRJ_SETTINGS = { 'CELERY_ROUTES': "app.celery.routes" @@ -255,7 +228,6 @@ update_celerybeat_schedule(CELERYBEAT_SCHEDULE, ENV_TOKENS, FIGURES_PIPELINE_TASKS_ROUTING_KEY) update_celery_routes(PRJ_SETTINGS, ENV_TOKENS, FIGURES_PIPELINE_TASKS_ROUTING_KEY) - # Used by Django Debug Toolbar INTERNAL_IPS = [ '127.0.0.1' @@ -266,7 +238,7 @@ 'NUM_LEARNERS_PER_COURSE': env('SEED_NUM_LEARNERS_PER_COURSE') } -ENABLE_OPENAPI_DOCS = env('ENABLE_OPENAPI_DOCS') and OPENEDX_RELEASE not in ['GINKGO', 'HAWTHORN'] +ENABLE_OPENAPI_DOCS = env('ENABLE_OPENAPI_DOCS') if ENABLE_OPENAPI_DOCS: - INSTALLED_APPS += ['drf_yasg2'] + INSTALLED_APPS += ['drf_yasg'] diff --git a/devsite/devsite/test_settings.py b/devsite/devsite/test_settings.py index cb567ef1..aa44c9a5 100644 --- a/devsite/devsite/test_settings.py +++ b/devsite/devsite/test_settings.py @@ -28,14 +28,14 @@ def root(*args): env = environ.Env( - OPENEDX_RELEASE=(str, 'JUNIPER'), + OPENEDX_RELEASE=(str, 'MAPLE'), ) environ.Env.read_env(join(dirname(dirname(__file__)), '.env')) OPENEDX_RELEASE = env('OPENEDX_RELEASE').upper() -MOCKS_DIR = 'mocks/{}'.format(OPENEDX_RELEASE.lower()) +MOCKS_DIR = 'mocks/' sys.path.append(root('mocks', MOCKS_DIR)) @@ -73,26 +73,19 @@ def root(*args): # Also note the paths set in edx-figures/pytest.ini 'openedx.core.djangoapps.content.course_overviews', 'openedx.core.djangoapps.course_groups', - 'student', + 'common.djangoapps.student', 'organizations' ] -if OPENEDX_RELEASE != 'GINGKO': - INSTALLED_APPS.append('djcelery') - - # We need this in order for figures.tasks unit tests to not fail with: - # "error: [Errno 61] Connection refused" - CELERY_ALWAYS_EAGER = True - -if OPENEDX_RELEASE == 'GINKGO': - INSTALLED_APPS.append('certificates') - INSTALLED_APPS.append('courseware') -elif OPENEDX_RELEASE == 'HAWTHORN': - INSTALLED_APPS.append('lms.djangoapps.certificates') - INSTALLED_APPS.append('courseware') -else: - INSTALLED_APPS.append('lms.djangoapps.certificates') - INSTALLED_APPS.append('lms.djangoapps.courseware') +# INSTALLED_APPS.append('djcelery') + +# We need this in order for figures.tasks unit tests to not fail with: +# "error: [Errno 61] Connection refused" +CELERY_ALWAYS_EAGER = True + + +INSTALLED_APPS.append('lms.djangoapps.certificates') +INSTALLED_APPS.append('lms.djangoapps.courseware') TEMPLATES = [ diff --git a/devsite/devsite/urls.py b/devsite/devsite/urls.py index f786bdfc..8b82f321 100644 --- a/devsite/devsite/urls.py +++ b/devsite/devsite/urls.py @@ -16,8 +16,8 @@ if settings.ENABLE_OPENAPI_DOCS: from rest_framework import permissions - from drf_yasg2.views import get_schema_view - from drf_yasg2 import openapi + from drf_yasg.views import get_schema_view + from drf_yasg import openapi schema_view = get_schema_view( openapi.Info( title="Figures API", diff --git a/devsite/requirements/juniper_base.txt b/devsite/requirements/base.txt similarity index 56% rename from devsite/requirements/juniper_base.txt rename to devsite/requirements/base.txt index c14a72e7..f22b876c 100644 --- a/devsite/requirements/juniper_base.txt +++ b/devsite/requirements/base.txt @@ -9,32 +9,32 @@ ## General Python package dependencies ### -celery==3.1.26.post2 -django-celery==3.3.1 -six==1.15.0 +celery==4.4.7 +six==1.16.0 # Faker is used to seed mock data in devsite -Faker==4.1.0 +Faker==4.1.0 # TODO python-dateutil==2.7.3 -path.py==12.4.0 +path.py==12.5.0 -pytz==2020.1 +pytz==2021.3 ## ## Django package dependencies ## -Django==2.2.27 -djangorestframework==3.9.4 -django-countries==5.5 +Django==3.2.10 + +djangorestframework==3.12.4 +django-countries==7.2.1 django-webpack-loader==0.7.0 -django-model-utils==4.0.0 -django-filter==2.3.0 -django-environ==0.4.5 -django-waffle==0.18.0 +django-model-utils==4.2.0 +django-filter==21.1 +django-environ==0.8.1 +django-waffle==2.2.1 -jsonfield==2.1.1 +jsonfield==3.1.0 # For @@ -46,11 +46,12 @@ jsonfield==2.1.1 Sphinx==3.1.2 #recommonmark==0.6.0 #! 0.4.0 #caniusepython3 flagged + ## ## Open edX package dependencies ## -edx-opaque-keys[django]==2.1.0 +edx-opaque-keys[django]==2.2.2 #edx-drf-extensions==6.0.0 @@ -58,24 +59,25 @@ edx-opaque-keys[django]==2.1.0 ## Devsite ## -django-debug-toolbar==2.2 - +django-debug-toolbar==2.2 # TODO +drf-yasg==1.20.0 +mysqlclient==2.1.0 ## ## Test dependencies ## -coverage==5.1 -factory-boy==2.8.1 +coverage==6.2 +factory-boy==3.2.0 flake8==3.8.1 -pylint==2.4.2 -pylint-django==2.0.11 -pytest==5.3.5 -pytest-django==3.8.0 -pytest-mock==3.2.0 -pytest-pythonpath==0.7.3 -pytest-cov==2.8.1 -tox==3.15.0 +pylint==2.9.6 +pylint-django==2.4.4 +pytest==6.2.5 +pytest-django==4.4.0 +pytest-mock==3.2.0 # TODO +pytest-pythonpath==0.7.3 # TODO +pytest-cov==3.0.0 +tox==3.24.4 freezegun==0.3.12 -edx-lint==1.4.1 -mock==3.0.5 +edx-lint==5.2.0 +mock==4.0.3 diff --git a/devsite/requirements/juniper_community.txt b/devsite/requirements/community.txt similarity index 56% rename from devsite/requirements/juniper_community.txt rename to devsite/requirements/community.txt index 1baf12dd..eb948729 100644 --- a/devsite/requirements/juniper_community.txt +++ b/devsite/requirements/community.txt @@ -1,5 +1,5 @@ # Requirements needed for Juniper community environment --r juniper_base.txt +-r base.txt -edx-organizations==5.2.0 +edx-organizations==6.10.0 diff --git a/devsite/requirements/development_juniper.txt b/devsite/requirements/development.txt similarity index 84% rename from devsite/requirements/development_juniper.txt rename to devsite/requirements/development.txt index c4e79ec3..5425fc59 100644 --- a/devsite/requirements/development_juniper.txt +++ b/devsite/requirements/development.txt @@ -1,6 +1,6 @@ # Requirements for developer environment with Python 3.8+ --r juniper_community.txt +-r community.txt # Used to serve OpenAPI docs # See Figures docs/source/api.rst diff --git a/devsite/requirements/ginkgo.txt b/devsite/requirements/ginkgo.txt deleted file mode 100644 index 072a756e..00000000 --- a/devsite/requirements/ginkgo.txt +++ /dev/null @@ -1,85 +0,0 @@ -# Requirements needed by the devsite app server and test suite -# For initial development, we're just importing all the packages needed -# for both running the devsite server and for the pytest dependencies -# - -# Versions should match those used in Open edX Ginkgo - -## -## General Python package dependencies -### - -celery==3.1.18 - -# Faker is used to seed mock data in devsite -Faker==1.0.4 -# Ginkgo requires python-dateutil==2.1 -# But this would require we use Faker version 0.5.3 or earlier -python-dateutil==2.4 -path.py==8.2.1 - -# Yes, this is old but is the one specified by Ginkgo edx-platform -pytz==2016.7 - -## -## Django package dependencies -## - -Django==1.8.18 -django-extensions==1.5.9 -# ./edx/base.txt:git+https://github.com/edx/django-rest-framework.git@3c72cb5ee5baebc4328947371195eae2077197b0#egg=djangorestframework==3.2.3 -djangorestframework==3.2.3 -django-countries==4.0 -django-filter==0.11.0 -django-webpack-loader==0.4.1 -# appsembler/gingko/master users 0.4.1 - -django-model-utils==2.3.1 -django-environ==0.4.5 -django-celery==3.2.1 -jsonfield==1.0.3 # Version used in Ginkgo. Hawthorn uses version 2.0.2 -django-waffle==0.12.0 - -## -## Documentation (Sphinx) dependencies -## - -Sphinx==1.8.1 -recommonmark==0.4.0 - -## -## Open edX package dependencies -## - -edx-opaque-keys==0.4 -# edx-organizations 0.4.4 requires edx-drf-extensions<1.0.0,>=0.5.1, but -# Ginkgo edx-platform open release specifies edx-drf-extensions 1.2.2 which is incompatible. We will use what's compatible with edx-organizations. -edx-drf-extensions==0.5.1 -edx-organizations==0.4.4 - - -## -## Devsite -## - -django-debug-toolbar==1.9.1 - - -## -## Pytest dependencies -## - -coverage==4.5.1 -factory-boy==2.5.1 -pylint==1.8.2 -pylint-django==0.9.1 -pytest==3.6.2 -pytest-django==3.1.2 -pytest-mock==1.7.1 -pytest-pythonpath==0.7.2 -pytest-cov==2.6.0 -tox==3.7.0 -freezegun==0.3.12 - -# Added to address: TypeError: attrib() got an unexpected keyword argument 'convert' -attrs==19.1.0 diff --git a/devsite/requirements/hawthorn.txt b/devsite/requirements/hawthorn.txt deleted file mode 100644 index 8f505050..00000000 --- a/devsite/requirements/hawthorn.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Python packages needed for Hawthorn community (single site operation) - --r hawthorn_base.txt - -# Use the community version of edx-organizations -edx-organizations==0.4.10 diff --git a/devsite/requirements/hawthorn_base.txt b/devsite/requirements/hawthorn_base.txt deleted file mode 100644 index 7dd3f92c..00000000 --- a/devsite/requirements/hawthorn_base.txt +++ /dev/null @@ -1,97 +0,0 @@ -# Base requirements needed for Hawthorn testing and devsite -# -# Versions should match those used in Open edX Hawthorn -# - -## -## General Python packages -## - -jsonfield==2.0.2 -path.py==8.2.1 -python-dateutil==2.7.3 - -# Yes, this is old but is the one specified by Hawthorn edx-platform -pytz==2016.10 - -## -## Django packages -## - -Django==1.11.29 -djangorestframework==3.6.3 -django-countries==4.6.1 -django-waffle==0.12.0 - -# edx-platform hawthorn does not use django-extensions -django-extensions==1.5.9 -django-environ==0.4.5 -django-filter==1.0.4 -django-model-utils==3.0.0 -django-webpack-loader==0.6.0 - -## -## Celery packages -## - -celery==3.1.25 -django-celery==3.3.1 - -## -## Open edX platform package dependencies -## - -edx-drf-extensions==1.5.2 -edx-opaque-keys==0.4.4 - -## -## Documentation (Sphinx) dependencies -## - -Sphinx==1.8.1 -recommonmark==0.4.0 - -## -## Devsite specific packages -## - -django-debug-toolbar==1.11 - -## -## Testing packages -## - -# To address: tox 3.14.2 requires pluggy<1,>=0.12.0, but you'll have pluggy 0.6.0 which is incompatible. -tox==3.1.0 -coverage==4.5.4 -factory-boy==2.5.1 -flake8==3.7.9 -mock==3.0.5 - -pytest==3.6.2 -pytest-django==3.1.2 -pytest-mock==1.7.1 -pytest-pythonpath==0.7.2 -pytest-cov==2.6.0 -freezegun==0.3.12 - -# Faker is used to seed mock data in devsite -Faker==2.0.3 - -# Added to address: TypeError: attrib() got an unexpected keyword argument 'convert' -attrs==19.1.0 - -## -## Linting packages -## - -# To address: edx-lint 0.5.5 requires pylint==1.7.1, but you'll have pylint 1.9.5 which is incompatible. -pylint==1.7.1 - -# To address edx-lint 0.5.5 requires pylint-django==0.7.2, but you'll have pylint-django 0.11.1 which is incompatible. -pylint-django==0.7.2 - -edx-lint==0.5.5 - -# To address: edx-lint 0.5.5 requires astroid==1.5.2, but you'll have astroid 1.6.6 which is incompatible. -astroid==1.5.2 diff --git a/devsite/requirements/hawthorn_multisite.txt b/devsite/requirements/hawthorn_multisite.txt deleted file mode 100644 index b61998b9..00000000 --- a/devsite/requirements/hawthorn_multisite.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Requirements needed for Hawthorn multisite environment - --r hawthorn_base.txt - -# Organization/site mapping requires Appsembler's fork -git+https://github.com/appsembler/edx-organizations.git@0.4.12-appsembler4 diff --git a/devsite/requirements/juniper_multisite.txt b/devsite/requirements/multisite.txt similarity index 63% rename from devsite/requirements/juniper_multisite.txt rename to devsite/requirements/multisite.txt index f4afbe91..28a73e19 100644 --- a/devsite/requirements/juniper_multisite.txt +++ b/devsite/requirements/multisite.txt @@ -1,6 +1,6 @@ -# Requirements needed for Juniper multisite environment +# Requirements needed for multisite environment --r juniper_base.txt +-r base.txt # Organization/site mapping requires Appsembler's fork git+https://github.com/appsembler/edx-organizations.git@5.2.0-appsembler13 diff --git a/figures/apps.py b/figures/apps.py index 9bbdbda9..4b69597a 100644 --- a/figures/apps.py +++ b/figures/apps.py @@ -8,29 +8,18 @@ from __future__ import absolute_import from django.apps import AppConfig -try: - from openedx.core.djangoapps.plugins.constants import ( - ProjectType, SettingsType, PluginURLs, PluginSettings - ) - PLATFORM_PLUGIN_SUPPORT = True -except ImportError: - # pre-hawthorn - PLATFORM_PLUGIN_SUPPORT = False - - -if PLATFORM_PLUGIN_SUPPORT: - def production_settings_name(): - """ - Helper for Hawthorn and Ironwood+ compatibility. - - This helper will explicitly break if something have changed in `SettingsType`. - """ - if hasattr(SettingsType, 'AWS'): - # Hawthorn and Ironwood - return getattr(SettingsType, 'AWS') - else: - # Juniper and beyond. - return getattr(SettingsType, 'PRODUCTION') +from openedx.core.djangoapps.plugins.constants import ( + ProjectType, SettingsType, PluginURLs, PluginSettings +) + + +def production_settings_name(): + """ + Helper for Hawthorn and Ironwood+ compatibility. + + This helper will explicitly break if something have changed in `SettingsType`. + """ + return getattr(SettingsType, 'PRODUCTION') class FiguresConfig(AppConfig): @@ -41,20 +30,19 @@ class FiguresConfig(AppConfig): name = 'figures' verbose_name = 'Figures' - if PLATFORM_PLUGIN_SUPPORT: - plugin_app = { - PluginURLs.CONFIG: { - ProjectType.LMS: { - PluginURLs.NAMESPACE: u'figures', - PluginURLs.REGEX: u'^figures/', - } - }, - - PluginSettings.CONFIG: { - ProjectType.LMS: { - production_settings_name(): { - PluginSettings.RELATIVE_PATH: u'settings.lms_production', - }, - } - }, - } + plugin_app = { + PluginURLs.CONFIG: { + ProjectType.LMS: { + PluginURLs.NAMESPACE: u'figures', + PluginURLs.REGEX: u'^figures/', + } + }, + + PluginSettings.CONFIG: { + ProjectType.LMS: { + production_settings_name(): { + PluginSettings.RELATIVE_PATH: u'settings.lms_production', + }, + } + }, + } diff --git a/figures/compat.py b/figures/compat.py index 30fef791..ccf3bc1c 100644 --- a/figures/compat.py +++ b/figures/compat.py @@ -27,6 +27,20 @@ from django.http import Http404 from figures.helpers import as_course_key +from openedx.core.release import RELEASE_LINE + +from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory # noqa pylint: disable=unused-import,import-error +from lms.djangoapps.certificates.models import GeneratedCertificate # noqa pylint: disable=unused-import,import-error +from lms.djangoapps.courseware.models import StudentModule # noqa pylint: disable=unused-import,import-error +from lms.djangoapps.courseware.courses import get_course_by_id # noqa pylint: disable=unused-import,import-error +from opaque_keys.edx.django.models import CourseKeyField # noqa pylint: disable=unused-import,import-error + +# preemptive addition. Added it here to avoid adding to figures.models +# In fact, we should probably do a refactoring that makes all Figures import it +# from here +from common.djangoapps.student.models import CourseAccessRole, CourseEnrollment # noqa pylint: disable=unused-import,import-error +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview # noqa pylint: disable=unused-import,import-error + class UnsuportedOpenedXRelease(Exception): pass @@ -38,46 +52,10 @@ class CourseNotFound(Exception): pass -# Pre-Ginkgo does not define `RELEASE_LINE` -try: - from openedx.core.release import RELEASE_LINE -except ImportError: +if RELEASE_LINE != 'maple': raise UnsuportedOpenedXRelease( - 'Unidentified Open edX release: ' - 'figures.compat could not import openedx.core.release.RELEASE_LINE') - - -if RELEASE_LINE == 'ginkgo': - from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory # noqa pylint: disable=unused-import,import-error -else: # Assume Hawthorn or greater - from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory # noqa pylint: disable=unused-import,import-error - -if RELEASE_LINE == 'ginkgo': - from certificates.models import GeneratedCertificate # noqa pylint: disable=unused-import,import-error -else: # Assume Hawthorn or greater - from lms.djangoapps.certificates.models import GeneratedCertificate # noqa pylint: disable=unused-import,import-error - -if RELEASE_LINE in ['ginkgo', 'hawthorn']: - from courseware.models import StudentModule # noqa pylint: disable=unused-import,import-error -else: # Assume Juniper or greater - from lms.djangoapps.courseware.models import StudentModule # noqa pylint: disable=unused-import,import-error - -if RELEASE_LINE in ['ginkgo', 'hawthorn']: - from courseware.courses import get_course_by_id # noqa pylint: disable=unused-import,import-error -else: # Assume Juniper or greater - from lms.djangoapps.courseware.courses import get_course_by_id # noqa pylint: disable=unused-import,import-error - -if RELEASE_LINE == 'ginkgo': - from openedx.core.djangoapps.xmodule_django.models import CourseKeyField # noqa pylint: disable=unused-import,import-error -else: # Assume Hawthorn or greater - from opaque_keys.edx.django.models import CourseKeyField # noqa pylint: disable=unused-import,import-error - - -# preemptive addition. Added it here to avoid adding to figures.models -# In fact, we should probably do a refactoring that makes all Figures import it -# from here -from student.models import CourseAccessRole, CourseEnrollment # noqa pylint: disable=unused-import,import-error -from openedx.core.djangoapps.content.course_overviews.models import CourseOverview # noqa pylint: disable=unused-import,import-error + 'The current Open edX is {}. This release of Figures requires Maple'.format( + RELEASE_LINE)) def course_grade(learner, course): @@ -86,10 +64,7 @@ def course_grade(learner, course): Returns the course grade for the specified learner and course """ - if RELEASE_LINE == 'ginkgo': - return CourseGradeFactory().create(learner, course) - else: # Assume Hawthorn or greater - return CourseGradeFactory().read(learner, course) + return CourseGradeFactory().read(learner, course) def course_grade_from_course_id(learner, course_id): @@ -118,10 +93,11 @@ def course_grade_from_course_id(learner, course_id): def chapter_grade_values(chapter_grades): ''' + **Maple upgrade note: We should be able to either do away with this function + or assume chapter_grades is a dict. ** Ginkgo introduced ``BlockUsageLocator``into the ``chapter_grades`` collection - For the current functionality we need, we can simply check if chapter_grades acts as a list or a dict ''' diff --git a/figures/filters.py b/figures/filters.py index d9ddfdd8..3baca15d 100644 --- a/figures/filters.py +++ b/figures/filters.py @@ -303,7 +303,7 @@ class UserFilterSet(django_filters.FilterSet): """Provides filtering for User model objects Note: User has a 1:1 relationship with the edx-platform LMS - student.models.UserProfile model + common.djangoapps.student.models.UserProfile model We're starting with a few fields and will add as we find we want/need them """ diff --git a/figures/models.py b/figures/models.py index 64192be6..a8f23ec1 100644 --- a/figures/models.py +++ b/figures/models.py @@ -11,7 +11,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import F -from django.utils.encoding import python_2_unicode_compatible + from jsonfield import JSONField @@ -30,7 +30,6 @@ def default_site(): return settings.SITE_ID -@python_2_unicode_compatible class CourseDailyMetrics(TimeStampedModel): """Metrics data specific to an individual course @@ -39,6 +38,7 @@ class CourseDailyMetrics(TimeStampedModel): adding a SiteDailyMetrics foreign key. This is subject to change as the code evolves. """ + # TODO: Review the most appropriate on_delete behaviour site = models.ForeignKey(Site, on_delete=models.CASCADE) date_for = models.DateField(db_index=True) @@ -56,9 +56,12 @@ class CourseDailyMetrics(TimeStampedModel): # that will save significant storage. Otherwise, we wait for the model # abstraction rework average_progress = models.DecimalField( - max_digits=3, decimal_places=2, blank=True, null=True, + max_digits=3, + decimal_places=2, + blank=True, + null=True, validators=[MaxValueValidator(1.0), MinValueValidator(0.0)], - ) + ) average_days_to_complete = models.IntegerField(blank=True, null=True) @@ -67,14 +70,21 @@ class CourseDailyMetrics(TimeStampedModel): num_learners_completed = models.IntegerField() class Meta: - unique_together = ('course_id', 'date_for',) - ordering = ('-date_for', 'course_id',) + unique_together = ( + "course_id", + "date_for", + ) + ordering = ( + "-date_for", + "course_id", + ) # Any other data we want? def __str__(self): return "id:{}, date_for:{}, course_id:{}".format( - self.id, self.date_for, self.course_id) + self.id, self.date_for, self.course_id + ) @classmethod def latest_previous_record(cls, site, course_id, date_for=None): @@ -89,11 +99,10 @@ def latest_previous_record(cls, site, course_id, date_for=None): filter_args = dict(site=site, course_id=course_id) if date_for: - filter_args['date_for__lt'] = date_for - return cls.objects.filter(**filter_args).order_by('-date_for').first() + filter_args["date_for__lt"] = date_for + return cls.objects.filter(**filter_args).order_by("-date_for").first() -@python_2_unicode_compatible class SiteDailyMetrics(TimeStampedModel): """ Stores metrics for a given site and day @@ -101,6 +110,7 @@ class SiteDailyMetrics(TimeStampedModel): When we upgrade to MAU 2G, we'll add a new field, 'active_users_today' and pull the data from the previous day's live active user capture """ + # TODO: Review the most appropriate on_delete behaviour site = models.ForeignKey(Site, on_delete=models.CASCADE) # Date for which this record's data are collected @@ -132,11 +142,13 @@ class Meta: Since we do want to constrain uniqueness per site+day, we'll need to fix this """ - ordering = ['-date_for', 'site'] + + ordering = ["-date_for", "site"] def __str__(self): return "id:{}, date_for:{}, site:{}".format( - self.id, self.date_for, self.site.domain) + self.id, self.date_for, self.site.domain + ) @classmethod def latest_previous_record(cls, site, date_for=None): @@ -151,18 +163,18 @@ def latest_previous_record(cls, site, date_for=None): filter_args = dict(site=site) if date_for: - filter_args['date_for__lt'] = date_for - recs = cls.objects.filter(**filter_args).order_by('-date_for') + filter_args["date_for__lt"] = date_for + recs = cls.objects.filter(**filter_args).order_by("-date_for") return recs[0] if recs else None -@python_2_unicode_compatible class SiteMonthlyMetrics(TimeStampedModel): """ Stores metrics for a given site and month """ + # TODO: Review the most appropriate on_delete behaviour site = models.ForeignKey(Site, on_delete=models.CASCADE) # Month for which this record's data are collected @@ -171,12 +183,13 @@ class SiteMonthlyMetrics(TimeStampedModel): active_user_count = models.IntegerField() class Meta: - ordering = ['-month_for', 'site'] - unique_together = ['month_for', 'site'] + ordering = ["-month_for", "site"] + unique_together = ["month_for", "site"] def __str__(self): return "id:{}, month_for:{}, site:{}".format( - self.id, self.month_for, self.site.domain) + self.id, self.month_for, self.site.domain + ) @classmethod def add_month(cls, site, year, month, active_user_count, overwrite=False): @@ -185,16 +198,18 @@ def add_month(cls, site, year, month, active_user_count, overwrite=False): if not overwrite: try: - obj = SiteMonthlyMetrics.objects.get(site=site, - month_for=month_for) - return (obj, False,) + obj = SiteMonthlyMetrics.objects.get(site=site, month_for=month_for) + return ( + obj, + False, + ) except SiteMonthlyMetrics.DoesNotExist: pass defaults = dict(active_user_count=active_user_count) - return SiteMonthlyMetrics.objects.update_or_create(site=site, - month_for=month_for, - defaults=defaults) + return SiteMonthlyMetrics.objects.update_or_create( + site=site, month_for=month_for, defaults=defaults + ) class EnrollmentDataManager(models.Manager): @@ -208,14 +223,18 @@ def set_enrollment_data(self, site, user, course_id, course_enrollment=None): """ This is an expensive call as it needs to call CourseGradeFactory if there is not already a LearnerCourseGradeMetrics record for the learner + + The `course_enrollment` parameter should be either a `CourseEnrollment` + queryset or `None` (`None` instead of `False`) """ + # TODO: improve this, but do it upstream if not course_enrollment: # For now, let it raise a `CourseEnrollment.DoesNotExist # Later on we can add a try block and raise out own custom # exception course_enrollment = CourseEnrollment.objects.get( - user=user, - course_id=as_course_key(course_id)) + user=user, course_id=as_course_key(course_id) + ) defaults = dict( is_enrolled=course_enrollment.is_active, @@ -224,8 +243,8 @@ def set_enrollment_data(self, site, user, course_id, course_enrollment=None): # Note: doesn't use site for filtering lcgm = LearnerCourseGradeMetrics.objects.latest_lcgm( - user=user, - course_id=str(course_id)) + user=user, course_id=str(course_id) + ) if lcgm: # do we already have an enrollment data record # We may change this to use @@ -236,7 +255,7 @@ def set_enrollment_data(self, site, user, course_id, course_enrollment=None): points_possible=lcgm.points_possible, points_earned=lcgm.points_earned, sections_possible=lcgm.sections_possible, - sections_worked=lcgm.sections_worked + sections_worked=lcgm.sections_worked, ) else: ep = EnrollmentProgress(user=user, course_id=course_id) @@ -246,18 +265,16 @@ def set_enrollment_data(self, site, user, course_id, course_enrollment=None): date_for=date.today(), is_completed=ep.is_completed(), progress_percent=ep.progress_percent(), - points_possible=ep.progress.get('points_possible', 0), - points_earned=ep.progress.get('points_earned', 0), - sections_possible=ep.progress.get('sections_possible', 0), - sections_worked=ep.progress.get('sections_worked', 0) + points_possible=ep.progress.get("points_possible", 0), + points_earned=ep.progress.get("points_earned", 0), + sections_possible=ep.progress.get("sections_possible", 0), + sections_worked=ep.progress.get("sections_worked", 0), ) defaults.update(progress_data) obj, created = self.update_or_create( - site=site, - user=user, - course_id=str(course_id), - defaults=defaults) + site=site, user=user, course_id=str(course_id), defaults=defaults + ) return obj, created def update_metrics(self, site, course_enrollment, force_update=False): @@ -326,7 +343,6 @@ def update_metrics(self, site, course_enrollment, force_update=False): return ed_recs[0], False -@python_2_unicode_compatible class EnrollmentData(TimeStampedModel): """Tracks most recent enrollment data for an enrollment @@ -345,9 +361,9 @@ class EnrollmentData(TimeStampedModel): with the basic Django architecture. Plus this simplifies running Figures on small Open edX LMS deployments """ + site = models.ForeignKey(Site, on_delete=models.CASCADE) - user = models.ForeignKey(settings.AUTH_USER_MODEL, - on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) course_id = models.CharField(max_length=255, db_index=True) date_for = models.DateField(db_index=True) @@ -373,11 +389,12 @@ class EnrollmentData(TimeStampedModel): objects = EnrollmentDataManager() class Meta: - unique_together = ('site', 'user', 'course_id') + unique_together = ("site", "user", "course_id") def __str__(self): - return '{} {} {} {}'.format( - self.id, self.site.domain, self.user.email, self.course_id) + return "{} {} {} {}".format( + self.id, self.site.domain, self.user.email, self.course_id + ) @property def progress_details(self): @@ -409,8 +426,9 @@ def latest_lcgm(self, user, course_id): TODO: Consider if we want to add 'site' as a parameter and update the uniqueness constraint to be: site, course_id, user, date_for """ - queryset = self.filter(user=user, - course_id=str(course_id)).order_by('-date_for') + queryset = self.filter(user=user, course_id=str(course_id)).order_by( + "-date_for" + ) return queryset[0] if queryset else None def most_recent_for_course(self, course_id): @@ -437,31 +455,30 @@ def completed_for_site(self, site, **_kwargs): filtering, since we can index on the field. However, we need to evaluate the additional storage need """ - qs = self.filter(site=site, - sections_possible__gt=0, - sections_worked=F('sections_possible')) + qs = self.filter( + site=site, sections_possible__gt=0, sections_worked=F("sections_possible") + ) # Build out filter. Note, we don't check if the var is iterable # we let it fail of invalid values passed in filter_args = dict() - user_ids = _kwargs.get('user_ids', None) + user_ids = _kwargs.get("user_ids", None) if user_ids: - filter_args['user_id__in'] = user_ids - course_ids = _kwargs.get('course_ids', None) + filter_args["user_id__in"] = user_ids + course_ids = _kwargs.get("course_ids", None) if course_ids: # We do the string casting in case couse_ids are CourseKey instance - filter_args['course_id__in'] = [str(key) for key in course_ids] + filter_args["course_id__in"] = [str(key) for key in course_ids] if filter_args: qs = qs.filter(**filter_args) return qs def completed_ids_for_site(self, site, **_kwargs): qs = self.completed_for_site(site, **_kwargs) - return qs.values('course_id', 'user_id').distinct() + return qs.values("course_id", "user_id").distinct() def completed_raw_for_site(self, site, **_kwargs): - """Experimental - """ + """Experimental""" statement = """ \ SELECT id, user_id, course_id, MAX(date_for) FROM figures_learnercoursegrademetrics lcgm @@ -474,7 +491,6 @@ def completed_raw_for_site(self, site, **_kwargs): return self.raw(statement.format(site=site)) -@python_2_unicode_compatible class LearnerCourseGradeMetrics(TimeStampedModel): """This model stores metrics for a learner and course on a given date @@ -506,15 +522,15 @@ class LearnerCourseGradeMetrics(TimeStampedModel): calculating it TODO: Add index on 'course_id', 'date_for', 'completed' """ + # TODO: Review the most appropriate on_delete behaviour site = models.ForeignKey(Site, on_delete=models.CASCADE) date_for = models.DateField(db_index=True) # TODO: We should require the user # TODO: Review the most appropriate on_delete behaviour - user = models.ForeignKey(settings.AUTH_USER_MODEL, - blank=True, - null=True, - on_delete=models.CASCADE) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.CASCADE + ) course_id = models.CharField(max_length=255, blank=True, db_index=True) points_possible = models.FloatField() points_earned = models.FloatField() @@ -531,12 +547,22 @@ class Meta: Do we want to add 'site' to the `unique_together` set? Open edX Course IDs are globally unique, so it is not required """ - unique_together = ('user', 'course_id', 'date_for',) - ordering = ('date_for', 'user__username', 'course_id',) + + unique_together = ( + "user", + "course_id", + "date_for", + ) + ordering = ( + "date_for", + "user__username", + "course_id", + ) def __str__(self): return "{} {} {} {}".format( - self.id, self.date_for, self.user.username, self.course_id) + self.id, self.date_for, self.user.username, self.course_id + ) @property def progress_percent(self): @@ -566,44 +592,43 @@ def progress_details(self): @property def completed(self): - return (self.sections_worked > 0 and - self.sections_worked == self.sections_possible) + return ( + self.sections_worked > 0 and self.sections_worked == self.sections_possible + ) -@python_2_unicode_compatible class PipelineError(TimeStampedModel): """ Captures errors when running Figures pipeline. TODO: Add organization foreign key when we add multi-tenancy """ - UNSPECIFIED_DATA = 'UNSPECIFIED' - GRADES_DATA = 'GRADES' - COURSE_DATA = 'COURSE' - SITE_DATA = 'SITE' + + UNSPECIFIED_DATA = "UNSPECIFIED" + GRADES_DATA = "GRADES" + COURSE_DATA = "COURSE" + SITE_DATA = "SITE" ERROR_TYPE_CHOICES = ( - (UNSPECIFIED_DATA, 'Unspecified data error'), - (GRADES_DATA, 'Grades data error'), - (COURSE_DATA, 'Course data error'), - (SITE_DATA, 'Site data error'), - ) + (UNSPECIFIED_DATA, "Unspecified data error"), + (GRADES_DATA, "Grades data error"), + (COURSE_DATA, "Course data error"), + (SITE_DATA, "Site data error"), + ) error_type = models.CharField( - max_length=255, choices=ERROR_TYPE_CHOICES, default=UNSPECIFIED_DATA) + max_length=255, choices=ERROR_TYPE_CHOICES, default=UNSPECIFIED_DATA + ) error_data = JSONField() # Attributes for convenient querying course_id = models.CharField(max_length=255, blank=True) # TODO: Review the most appropriate on_delete behaviour - user = models.ForeignKey(settings.AUTH_USER_MODEL, - blank=True, - null=True, - on_delete=models.CASCADE) - site = models.ForeignKey(Site, blank=True, - null=True, - on_delete=models.CASCADE) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.CASCADE + ) + site = models.ForeignKey(Site, blank=True, null=True, on_delete=models.CASCADE) class Meta: - ordering = ['-created'] + ordering = ["-created"] def __str__(self): return "{}, {}, {}".format(self.id, self.created, self.error_type) @@ -616,7 +641,7 @@ class BaseDateMetricsModel(TimeStampedModel): class Meta: abstract = True - ordering = ['-date_for'] + ordering = ["-date_for"] @property def year(self): @@ -628,26 +653,26 @@ def month(self): class SiteMauMetricsManager(models.Manager): - """Custom model manager for SiteMauMMetrics model - """ + """Custom model manager for SiteMauMMetrics model""" + def latest_for_site_month(self, site, year, month): """Return the latest record for the given site, month, and year If no record found, returns 'None' """ - queryset = self.filter(site=site, - date_for__year=year, - date_for__month=month) - return queryset.order_by('-modified').first() + queryset = self.filter(site=site, date_for__year=year, date_for__month=month) + return queryset.order_by("-modified").first() -@python_2_unicode_compatible class SiteMauMetrics(BaseDateMetricsModel): mau = models.IntegerField() objects = SiteMauMetricsManager() class Meta: - unique_together = ('site', 'date_for',) + unique_together = ( + "site", + "date_for", + ) @classmethod def save_metrics(cls, site, date_for, data, overwrite=False): @@ -658,25 +683,26 @@ def save_metrics(cls, site, date_for, data, overwrite=False): if not overwrite: try: obj = SiteMauMetrics.objects.get(site=site, date_for=date_for) - return (obj, False,) + return ( + obj, + False, + ) except SiteMauMetrics.DoesNotExist: pass - return SiteMauMetrics.objects.update_or_create(site=site, - date_for=date_for, - defaults=dict( - mau=data['mau'])) + return SiteMauMetrics.objects.update_or_create( + site=site, date_for=date_for, defaults=dict(mau=data["mau"]) + ) def __str__(self): - return '{}, {}, {}, {}'.format(self.id, - self.site.domain, - self.date_for, - self.mau) + return "{}, {}, {}, {}".format( + self.id, self.site.domain, self.date_for, self.mau + ) class CourseMauMetricsManager(models.Manager): - """Custom model manager for CourseMauMetrics model - """ + """Custom model manager for CourseMauMetrics model""" + def latest_for_course_month(self, site, course_id, year, month): """Return the latest record for the given site, course_id, month and year If no record found, returns 'None' @@ -687,17 +713,20 @@ def latest_for_course_month(self, site, course_id, year, month): date_for__year=year, date_for__month=month, ) - return queryset.order_by('-modified').first() + return queryset.order_by("-modified").first() -@python_2_unicode_compatible class CourseMauMetrics(BaseDateMetricsModel): course_id = models.CharField(max_length=255) mau = models.IntegerField() objects = CourseMauMetricsManager() class Meta: - unique_together = ('site', 'course_id', 'date_for',) + unique_together = ( + "site", + "course_id", + "date_for", + ) @classmethod def save_metrics(cls, site, course_id, date_for, data, overwrite=False): @@ -707,22 +736,24 @@ def save_metrics(cls, site, course_id, date_for, data, overwrite=False): """ if not overwrite: try: - obj = CourseMauMetrics.objects.get(site=site, - course_id=course_id, - date_for=date_for) - return (obj, False,) + obj = CourseMauMetrics.objects.get( + site=site, course_id=course_id, date_for=date_for + ) + return ( + obj, + False, + ) except CourseMauMetrics.DoesNotExist: pass - return CourseMauMetrics.objects.update_or_create(site=site, - course_id=course_id, - date_for=date_for, - defaults=dict( - mau=data['mau'])) + return CourseMauMetrics.objects.update_or_create( + site=site, + course_id=course_id, + date_for=date_for, + defaults=dict(mau=data["mau"]), + ) def __str__(self): - return '{}, {}, {}, {}, {}'.format(self.id, - self.site.domain, - self.course_id, - self.date_for, - self.mau) + return "{}, {}, {}, {}, {}".format( + self.id, self.site.domain, self.course_id, self.date_for, self.mau + ) diff --git a/figures/pipeline/course_daily_metrics.py b/figures/pipeline/course_daily_metrics.py index 95d79f94..9cd3297a 100644 --- a/figures/pipeline/course_daily_metrics.py +++ b/figures/pipeline/course_daily_metrics.py @@ -17,7 +17,7 @@ from dateutil.relativedelta import relativedelta from django.db import transaction -from student.roles import CourseCcxCoachRole, CourseInstructorRole, CourseStaffRole # noqa pylint: disable=import-error +from common.djangoapps.student.roles import CourseCcxCoachRole, CourseInstructorRole, CourseStaffRole # noqa pylint: disable=import-error from figures.compat import (CourseEnrollment, CourseOverview, diff --git a/figures/settings/lms_production.py b/figures/settings/lms_production.py index fd7c3b5d..6a1b48c2 100644 --- a/figures/settings/lms_production.py +++ b/figures/settings/lms_production.py @@ -21,10 +21,7 @@ def route_for_task(self, task, args=None, kwargs=None): # pylint: disable=unuse def get_build_label(release_line): - if release_line in ['ginkgo', 'hawthorn']: - return 'rb10' - else: - return 'rb16' + return 'rb16' def update_webpack_loader(webpack_loader_settings, figures_env_tokens): diff --git a/figures/sites.py b/figures/sites.py index e8be5344..a2b491a2 100644 --- a/figures/sites.py +++ b/figures/sites.py @@ -157,7 +157,7 @@ def site_course_ids(site): """ if is_multisite(): return organizations.models.OrganizationCourse.objects.filter( - organization__sites__in=[site]).values_list('course_id', flat=True) + organization__sites__in=[site]).values_list('course_id', flat=True) else: # Needs work. See about returning a queryset return [str(key) for key in CourseOverview.objects.all().values_list( diff --git a/figures/urls.py b/figures/urls.py index 5e61ada4..4131382c 100644 --- a/figures/urls.py +++ b/figures/urls.py @@ -16,54 +16,54 @@ router.register( r'course-daily-metrics', views.CourseDailyMetricsViewSet, - base_name='course-daily-metrics') + basename='course-daily-metrics') router.register( r'site-daily-metrics', views.SiteDailyMetricsViewSet, - base_name='site-daily-metrics') + basename='site-daily-metrics') router.register( r'course-monthly-metrics', views.CourseMonthlyMetricsViewSet, - base_name='course-monthly-metrics') + basename='course-monthly-metrics') router.register( r'site-monthly-metrics', views.SiteMonthlyMetricsViewSet, - base_name='site-monthly-metrics') + basename='site-monthly-metrics') router.register( r'course-mau-metrics', views.CourseMauMetricsViewSet, - base_name='course-mau-metrics') + basename='course-mau-metrics') router.register( r'site-mau-metrics', views.SiteMauMetricsViewSet, - base_name='site-mau-metrics') + basename='site-mau-metrics') router.register( r'course-mau-live-metrics', views.CourseMauLiveMetricsViewSet, - base_name='course-mau-live-metrics') + basename='course-mau-live-metrics') router.register( r'site-mau-live-metrics', views.SiteMauLiveMetricsViewSet, - base_name='site-mau-live-metrics') + basename='site-mau-live-metrics') router.register( r'admin/sites', views.SiteViewSet, - base_name='sites') + basename='sites') # Wrappers around edx-platform models router.register( r'course-enrollments', views.CourseEnrollmentViewSet, - base_name='course-enrollments') + basename='course-enrollments') # @@ -74,27 +74,27 @@ router.register( r'courses-index', views.CoursesIndexViewSet, - base_name='courses-index') + basename='courses-index') router.register( r'courses-general', views.GeneralCourseDataViewSet, - base_name='courses-general') + basename='courses-general') router.register( r'courses-detail', views.CourseDetailsViewSet, - base_name='courses-detail') + basename='courses-detail') router.register( r'users-general', views.GeneralUserDataViewSet, - base_name='users-general') + basename='users-general') router.register( r'users-detail', views.LearnerDetailsViewSet, - base_name='users-detail') + basename='users-detail') # TODO: Consider changing this path to be 'users' or 'users/summary' # So that all user data fall under the same root path @@ -102,7 +102,7 @@ router.register( r'user-index', views.UserIndexViewSet, - base_name='user-index') + basename='user-index') # New endpoints in development (unstable) @@ -111,17 +111,17 @@ router.register( r'enrollment-metrics', views.EnrollmentMetricsViewSet, - base_name='enrollment-metrics') + basename='enrollment-metrics') router.register( r'learner-metrics-v1', views.LearnerMetricsViewSetV1, - base_name='learner-metrics-v1') + basename='learner-metrics-v1') router.register( r'learner-metrics', views.LearnerMetricsViewSetV2, - base_name='learner-metrics') + basename='learner-metrics') urlpatterns = [ diff --git a/figures/views.py b/figures/views.py index d9ec7eed..3de1264e 100644 --- a/figures/views.py +++ b/figures/views.py @@ -17,7 +17,7 @@ SessionAuthentication, TokenAuthentication, ) -from rest_framework.decorators import detail_route, list_route +from rest_framework.decorators import action from rest_framework.exceptions import NotFound from rest_framework.permissions import IsAuthenticated @@ -524,7 +524,7 @@ def get_queryset(self): queryset = LearnerCourseGradeMetrics.objects.filter(site=site) return queryset - @list_route() + @action(detail=False) def completed_ids(self, request): """Return distinct course id/user id pairs for completed enrollments @@ -542,7 +542,7 @@ def completed_ids(self, request): serializer = CourseCompletedSerializer(qs, many=True) return Response(serializer.data) - @list_route() + @action(detail=False) def completed(self, request): """Experimental endpoint to return completed LCGM records @@ -632,7 +632,7 @@ def retrieve(self, request, **kwargs): # pylint: disable=unused-argument month_for=month_for) return Response(data) - @detail_route() + @action(detail=True) def active_users(self, request, **kwargs): # pylint: disable=unused-argument site, course_id = self.site_course_helper(kwargs.get('pk', '')) date_for = datetime.utcnow().date() @@ -646,7 +646,7 @@ def active_users(self, request, **kwargs): # pylint: disable=unused-argument data = dict(active_users=active_users) return Response(data) - @detail_route() + @action(detail=True) def course_enrollments(self, request, **kwargs): site, course_id = self.site_course_helper(kwargs.get('pk', '')) data = dict(course_enrollments=self.historic_data( @@ -656,7 +656,7 @@ def course_enrollments(self, request, **kwargs): func=metrics.get_course_enrolled_users_for_time_period)) return Response(data) - @detail_route() + @action(detail=True) def num_learners_completed(self, request, **kwargs): site, course_id = self.site_course_helper(kwargs.get('pk', '')) data = dict(num_learners_completed=self.historic_data( @@ -666,7 +666,7 @@ def num_learners_completed(self, request, **kwargs): func=metrics.get_course_num_learners_completed_for_time_period)) return Response(data) - @detail_route() + @action(detail=True) def avg_days_to_complete(self, request, **kwargs): site, course_id = self.site_course_helper(kwargs.get('pk', '')) data = dict(avg_days_to_complete=self.historic_data( @@ -676,7 +676,7 @@ def avg_days_to_complete(self, request, **kwargs): func=metrics.get_course_average_days_to_complete_for_time_period)) return Response(data) - @detail_route() + @action(detail=True) def avg_progress(self, request, **kwargs): site, course_id = self.site_course_helper(kwargs.get('pk', '')) data = dict(avg_progress=self.historic_data( @@ -714,7 +714,7 @@ def list(self, request): data = metrics.get_current_month_site_metrics(site) return Response(data) - @list_route() + @action(detail=False) def registered_users(self, request): site = figures.sites.get_requested_site(request) date_for = datetime.utcnow().date() @@ -729,7 +729,7 @@ def registered_users(self, request): data = dict(registered_users=registered_users) return Response(data) - @list_route() + @action(detail=False) def new_users(self, request): """ TODO: Rename the metrics module function to "new_users" to match this @@ -747,7 +747,7 @@ def new_users(self, request): data = dict(new_users=new_users) return Response(data) - @list_route() + @action(detail=False) def course_completions(self, request): site = figures.sites.get_requested_site(request) date_for = datetime.utcnow().date() @@ -762,7 +762,7 @@ def course_completions(self, request): data = dict(course_completions=course_completions) return Response(data) - @list_route() + @action(detail=False) def course_enrollments(self, request): site = figures.sites.get_requested_site(request) date_for = datetime.utcnow().date() @@ -777,7 +777,7 @@ def course_enrollments(self, request): data = dict(course_enrollments=course_enrollments) return Response(data) - @list_route() + @action(detail=False) def site_courses(self, request): site = figures.sites.get_requested_site(request) date_for = datetime.utcnow().date() @@ -792,7 +792,7 @@ def site_courses(self, request): data = dict(site_courses=site_courses) return Response(data) - @list_route() + @action(detail=False) def active_users(self, request): site = figures.sites.get_requested_site(request) months_back = 6 diff --git a/ginkgo-env b/ginkgo-env deleted file mode 100644 index 33ceb167..00000000 --- a/ginkgo-env +++ /dev/null @@ -1 +0,0 @@ -export OPENEDX_RELEASE='GINKGO' diff --git a/mocks/ginkgo/__init__.py b/mocks/__init__.py similarity index 100% rename from mocks/ginkgo/__init__.py rename to mocks/__init__.py diff --git a/mocks/ginkgo/certificates/__init__.py b/mocks/common/djangoapps/student/__init__.py similarity index 100% rename from mocks/ginkgo/certificates/__init__.py rename to mocks/common/djangoapps/student/__init__.py diff --git a/mocks/juniper/student/migrations/0001_initial.py b/mocks/common/djangoapps/student/migrations/0001_initial.py similarity index 100% rename from mocks/juniper/student/migrations/0001_initial.py rename to mocks/common/djangoapps/student/migrations/0001_initial.py diff --git a/mocks/ginkgo/course_modes/__init__.py b/mocks/common/djangoapps/student/migrations/__init__.py similarity index 100% rename from mocks/ginkgo/course_modes/__init__.py rename to mocks/common/djangoapps/student/migrations/__init__.py diff --git a/mocks/juniper/student/models.py b/mocks/common/djangoapps/student/models.py similarity index 89% rename from mocks/juniper/student/models.py rename to mocks/common/djangoapps/student/models.py index 639ea24e..f4a73875 100644 --- a/mocks/juniper/student/models.py +++ b/mocks/common/djangoapps/student/models.py @@ -7,7 +7,7 @@ import six from django.db import models from django.contrib.auth.models import User -from django.utils.translation import ugettext_noop +from django.utils.translation import gettext_noop from django_countries.fields import CountryField @@ -23,7 +23,7 @@ class UserProfile(models.Model): ''' - The production model is student.models.UserProfile + The production model is common.djangoapps.student.models.UserProfile ''' user = models.OneToOneField(User, unique=True, db_index=True, related_name='profile', on_delete=models.CASCADE) @@ -42,10 +42,10 @@ class UserProfile(models.Model): year_of_birth = models.IntegerField(blank=True, null=True, db_index=True) GENDER_CHOICES = ( - (u'm', ugettext_noop(u'Male')), - (u'f', ugettext_noop(u'Female')), + (u'm', gettext_noop(u'Male')), + (u'f', gettext_noop(u'Female')), # Translators: 'Other' refers to the student's gender - (u'o', ugettext_noop(u'Other/Prefer Not to Say')) + (u'o', gettext_noop(u'Other/Prefer Not to Say')) ) gender = models.CharField( blank=True, null=True, max_length=6, db_index=True, choices=GENDER_CHOICES @@ -56,17 +56,17 @@ class UserProfile(models.Model): # ('p_se', 'Doctorate in science or engineering'), # ('p_oth', 'Doctorate in another field'), LEVEL_OF_EDUCATION_CHOICES = ( - (u'p', ugettext_noop(u'Doctorate')), - (u'm', ugettext_noop(u"Master's or professional degree")), - (u'b', ugettext_noop(u"Bachelor's degree")), - (u'a', ugettext_noop(u"Associate degree")), - (u'hs', ugettext_noop(u"Secondary/high school")), - (u'jhs', ugettext_noop(u"Junior secondary/junior high/middle school")), - (u'el', ugettext_noop(u"Elementary/primary school")), + (u'p', gettext_noop(u'Doctorate')), + (u'm', gettext_noop(u"Master's or professional degree")), + (u'b', gettext_noop(u"Bachelor's degree")), + (u'a', gettext_noop(u"Associate degree")), + (u'hs', gettext_noop(u"Secondary/high school")), + (u'jhs', gettext_noop(u"Junior secondary/junior high/middle school")), + (u'el', gettext_noop(u"Elementary/primary school")), # Translators: 'None' refers to the student's level of education - (u'none', ugettext_noop(u"No formal education")), + (u'none', gettext_noop(u"No formal education")), # Translators: 'Other' refers to the student's level of education - (u'other', ugettext_noop(u"Other education")) + (u'other', gettext_noop(u"Other education")) ) level_of_education = models.CharField( blank=True, null=True, max_length=6, db_index=True, @@ -103,7 +103,7 @@ def num_enrolled_in_exclude_admins(self, course_id): """ # To avoid circular imports. - from student.roles import CourseCcxCoachRole, CourseInstructorRole, CourseStaffRole + from common.djangoapps.student.roles import CourseCcxCoachRole, CourseInstructorRole, CourseStaffRole course_locator = course_id if getattr(course_id, 'ccx', None): @@ -135,7 +135,7 @@ def enrollment_counts(self, course_id): class CourseEnrollment(models.Model): ''' - The production model is student.models.CourseEnrollment + The production model is common.djangoapps.student.models.CourseEnrollment The purpose of this mock is to provide the model needed to retrieve: diff --git a/mocks/juniper/student/roles.py b/mocks/common/djangoapps/student/roles.py similarity index 79% rename from mocks/juniper/student/roles.py rename to mocks/common/djangoapps/student/roles.py index ef168281..12c0a9c5 100644 --- a/mocks/juniper/student/roles.py +++ b/mocks/common/djangoapps/student/roles.py @@ -1,26 +1,28 @@ -''' +""" Mocks role classes needed in Figures tests -''' +""" from __future__ import absolute_import from django.contrib.auth.models import User from opaque_keys.edx.django.models import CourseKeyField -from student.models import CourseAccessRole +from common.djangoapps.student.models import CourseAccessRole + class MockCourseRole(object): - ''' - Mock student.models.CourseRole and its parent classes + """ + Mock common.djangoapps.student.models.CourseRole and its parent classes Guideline: only implement the minimum needed to simulate edx-platform for the Figures unit tests - ''' + """ + def __init__(self, role, course_key): # The following are declared in studen.roles.RoleBase - self.org = '' + self.org = "" self._role_name = role - # The following are declared in student.roles.CourseRole + # The following are declared in common.djangoapps.student.roles.CourseRole self.role = role self.course_key = course_key @@ -34,27 +36,27 @@ def users_with_role(self): entries = User.objects.filter( courseaccessrole__role=self._role_name, courseaccessrole__org=self.org, - courseaccessrole__course_id=self.course_key + courseaccessrole__course_id=self.course_key, ) return entries class CourseCcxCoachRole(MockCourseRole): - ROLE = 'ccx_coach' + ROLE = "ccx_coach" def __init__(self, *args, **kwargs): super(CourseCcxCoachRole, self).__init__(self.ROLE, *args, **kwargs) class CourseInstructorRole(MockCourseRole): - ROLE = 'instructor' + ROLE = "instructor" def __init__(self, *args, **kwargs): super(CourseInstructorRole, self).__init__(self.ROLE, *args, **kwargs) class CourseStaffRole(MockCourseRole): - ROLE = 'staff' + ROLE = "staff" def __init__(self, *args, **kwargs): super(CourseStaffRole, self).__init__(self.ROLE, *args, **kwargs) diff --git a/mocks/ginkgo/courseware/__init__.py b/mocks/course_modes/__init__.py similarity index 100% rename from mocks/ginkgo/courseware/__init__.py rename to mocks/course_modes/__init__.py diff --git a/mocks/juniper/course_modes/models.py b/mocks/course_modes/models.py similarity index 100% rename from mocks/juniper/course_modes/models.py rename to mocks/course_modes/models.py diff --git a/mocks/ginkgo/certificates/models.py b/mocks/ginkgo/certificates/models.py deleted file mode 100644 index 6f04798e..00000000 --- a/mocks/ginkgo/certificates/models.py +++ /dev/null @@ -1,11 +0,0 @@ - -from __future__ import absolute_import -from django.db import models -from django.contrib.auth.models import User - -from openedx.core.djangoapps.xmodule_django.models import CourseKeyField - -class GeneratedCertificate(models.Model): - user = models.ForeignKey(User) - course_id = CourseKeyField(max_length=255, blank=True, default=None) - created_date = models.DateTimeField() diff --git a/mocks/ginkgo/course_modes/models.py b/mocks/ginkgo/course_modes/models.py deleted file mode 100644 index 458d60e5..00000000 --- a/mocks/ginkgo/course_modes/models.py +++ /dev/null @@ -1,13 +0,0 @@ - - - -class CourseMode(object): - ''' - In edx-platform, this is a Django Model. - - Currently we only need to mock getting the - default mode slug - ''' - - # - DEFAULT_MODE_SLUG = 'audit' \ No newline at end of file diff --git a/mocks/ginkgo/courseware/courses.py b/mocks/ginkgo/courseware/courses.py deleted file mode 100644 index 405db98a..00000000 --- a/mocks/ginkgo/courseware/courses.py +++ /dev/null @@ -1,26 +0,0 @@ -''' - -./common/lib/xmodule/xmodule/modulestore -''' - -from __future__ import absolute_import -from django.http import Http404 -from xmodule.modulestore.django import modulestore -import six - - -def get_course_by_id(course_key, depth=0): - """ - Given a course id, return the corresponding course descriptor. - - If such a course does not exist, raises a 404. - - depth: The number of levels of children for the modulestore to cache. None means infinite depth - """ - with modulestore().bulk_operations(course_key): - course = modulestore().get_course(course_key, depth=depth) - - if course: - return course - else: - raise Http404("Course not found: {}.".format(six.text_type(course_key))) diff --git a/mocks/ginkgo/courseware/models.py b/mocks/ginkgo/courseware/models.py deleted file mode 100644 index e500788e..00000000 --- a/mocks/ginkgo/courseware/models.py +++ /dev/null @@ -1,59 +0,0 @@ - -from __future__ import absolute_import -from django.db import models -from django.contrib.auth.models import User - -from openedx.core.djangoapps.xmodule_django.models import ( - CourseKeyField, - #! LocationKeyField, - ) - - -class StudentModule(models.Model): - '''Mocks the courseware.models.StudentModule - - class attributes declared in StudentModule but not yet - needed for mocking are remarked out with a '#!' - They are here to - A) Help understand context of the class without requiring opening the - courseware/models.py file - B) Be available to quickly update this mock when needed - ''' - #! MODEL_TAGS = ['course_id', 'module_type'] - - # For a homework problem, contains a JSON - # object consisting of state - #! MODULE_TYPES = (('problem', 'problem'), - #! ('video', 'video'), - #! ('html', 'html'), - #! ('course', 'course'), - #! ('chapter', 'Section'), - #! ('sequential', 'Subsection'), - #! ('library_content', 'Library Content')) - - #! module_state_key = LocationKeyField(max_length=255, db_index=True, db_column='module_id') - - student = models.ForeignKey(User, db_index=True) - - course_id = CourseKeyField(max_length=255, db_index=True) - - #! class Meta(object): - #! app_label = "courseware" - #! unique_together = (('student', 'module_state_key', 'course_id'),) - - #! # Internal state of the object - #! state = models.TextField(null=True, blank=True) - - #! # Grade, and are we done? - #! grade = models.FloatField(null=True, blank=True, db_index=True) - #! max_grade = models.FloatField(null=True, blank=True) - #! DONE_TYPES = ( - #! ('na', 'NOT_APPLICABLE'), - #! ('f', 'FINISHED'), - #! ('i', 'INCOMPLETE'), - #! ) - #! done = models.CharField(max_length=8, choices=DONE_TYPES, default='na', db_index=True) - - # the production model sets 'auto_now_add=True' andn 'db_index=True' - created = models.DateTimeField() - modified = models.DateTimeField() diff --git a/mocks/ginkgo/lms/djangoapps/grades/new/course_grade.py b/mocks/ginkgo/lms/djangoapps/grades/new/course_grade.py deleted file mode 100644 index eeacb633..00000000 --- a/mocks/ginkgo/lms/djangoapps/grades/new/course_grade.py +++ /dev/null @@ -1,95 +0,0 @@ -''' - -''' -from __future__ import absolute_import -from collections import OrderedDict - - -class MockAggregatedScore(object): - ''' - - ''' - def __init__(self, tw_earned, tw_possible, **kwargs): - self.graded = False - self.first_attempted = None - self.earned = float(tw_earned) if tw_earned is not None else None - self.possible = float(tw_possible) if tw_possible is not None else None - - def __str__(self): - return 'earned={}, possible={}, graded={}, first_attempted={}'.format( - self.earned, self.possible, self.graded, self.first_attempted) - - def __repr__(self): - return self.__str__() - - -class MockSubsectionGrade(object): - ''' - - ''' - def __init__(self, **kwargs): - self.problem_scores = OrderedDict() - self.all_total = MockAggregatedScore( - tw_earned=kwargs.get('tw_earned', 0.0), - tw_possible=kwargs.get('tw_possible', 0.0) - ) - - def __str__(self): - return 'all_total={}'.format(self.all_total) - - def __repr__(self): - return self.__str__() - -def create_chapter_grades(): - ''' - Mock for course_grades.CourseGradeBase.chapter_grades - (which uses the @lazy decorator) - - for chapter_grade in self.course_grade.chapter_grades.values(): - for section in chapter_grade['sections']: - - we don't need the BlockUsageLocator key for our initial testing - So we're just going to use the phonetic alphabet - - Use the following to generate additional url names - ``binascii.b2a_hex(os.urandom(16))`` - - Chapter grade keys are 'sections', 'url_name', and 'display_name' - ''' - - return OrderedDict( - alpha=dict( - sections=[ - MockSubsectionGrade(tw_earned=0.0, tw_possible=0.0), - MockSubsectionGrade(tw_earned=0.0,tw_possible=0.5), - MockSubsectionGrade(tw_earned=0.5,tw_possible=1.0), - ], - url_name=u'ec7e84694fca2d073731a462a5916a7a', - display_name=u'Module 1 - Overview', - ), - bravo=dict( - sections=[ - MockSubsectionGrade(), - MockSubsectionGrade(), - MockSubsectionGrade(), - ], - url_name=u'2f43c0b7da59ed40156155f9a8ca4d40', - display_name=u'Module 2 - First Principles', - ), - ) - - -class CourseGrade(object): - ''' - Production class inherits from CourseGradeBase - ''' - def __init__(self, user, course_data, percent=0, letter_grade=None, passed=False, *args, **kwargs): - self.user = user - self.course_data = course_data, - self.percent = percent - self.passed = passed - - # Convert empty strings to None when reading from the table - self.letter_grade = letter_grade or None - self.chapter_grades = kwargs.get('chapter_grades', - create_chapter_grades()) diff --git a/mocks/ginkgo/lms/djangoapps/grades/new/course_grade_factory.py b/mocks/ginkgo/lms/djangoapps/grades/new/course_grade_factory.py deleted file mode 100644 index 260133ca..00000000 --- a/mocks/ginkgo/lms/djangoapps/grades/new/course_grade_factory.py +++ /dev/null @@ -1,25 +0,0 @@ - - -from .course_grade import CourseGrade - - -class MockCourseData(object): - - def __init__(self, user, course=None, collected_block_structure=None, structure=None, course_key=None): - if not any([course, collected_block_structure, structure, course_key]): - raise ValueError( - "You must specify one of course, collected_block_structure, structure, or course_key to this method." - ) - self.user = user - self._collected_block_structure = collected_block_structure - self._structure = structure - self._course = course - self._course_key = course_key - self._location = None - - -class CourseGradeFactory(object): - - def create(self, user, course=None, collected_block_structure=None, course_structure=None, course_key=None): - course_data = MockCourseData(user, course, collected_block_structure, course_structure, course_key) - return CourseGrade(user, course_data, force_update_subsections=False) diff --git a/mocks/ginkgo/lms/djangoapps/teams/models.py b/mocks/ginkgo/lms/djangoapps/teams/models.py deleted file mode 100644 index 30439d3a..00000000 --- a/mocks/ginkgo/lms/djangoapps/teams/models.py +++ /dev/null @@ -1,23 +0,0 @@ - -from __future__ import absolute_import -from django.db import models -from django.contrib.auth.models import User - -class CourseTeam(models.Model): - - class Meta: - app_label = 'teams' - - name = models.CharField(max_length=255, db_index=True) - users = models.ManyToManyField(User, - db_index=True, related_name='teams', through='CourseTeamMembership') - -class CourseTeamMembership(models.Model): - - class Meta: - app_label = "teams" - unique_together = (('user', 'team'),) - - user = models.ForeignKey(User) - team = models.ForeignKey(CourseTeam, related_name='membership') - diff --git a/mocks/ginkgo/openedx/core/djangoapps/content/course_overviews/models.py b/mocks/ginkgo/openedx/core/djangoapps/content/course_overviews/models.py deleted file mode 100644 index c0a4d4b9..00000000 --- a/mocks/ginkgo/openedx/core/djangoapps/content/course_overviews/models.py +++ /dev/null @@ -1,76 +0,0 @@ -''' -Provides fake models for openedx.core.djangoapps.content.course_overviews - -Overview --------- - -The purpose of this module is to provide the minimum models in order to mock -Figures access to edx-platform models - -Reference ---------- - -See also the lms.djangoapps.course_api.serializers.CourseSerializer class - -This provides the data that are returned in the built-in edx-platform -course_api REST API calls - -Future ------- - -If and when openedx.core is re-architected to be independent from edx-platform, -or we can selectively include apps from edx-platform without requiring complex -test settings and a filesystem based test infrastructure, then we can revisit -removing these mocks - -''' - -from __future__ import absolute_import -from django.db import models - -from openedx.core.djangoapps.xmodule_django.models import CourseKeyField - -class CourseOverview(models.Model): - ''' - Provides a mock model for the edx-platform 'CourseOverview' model - - Future Improvements - ------------------- - - We want to provide enhanced live querying like - - "Which courses are invitation only?" - - "Which courses have a maximum allowed enrollment above X" - - ''' - # Faking id, picking arbitrary length - # Actual field is of type opaque_keys.edx.keys.CourseKey - #id = models.CharField(db_index=True, primary_key=True, max_length=255) - id = CourseKeyField(db_index=True, primary_key=True, max_length=255) - display_name = models.TextField(null=True) - org = models.TextField(max_length=255, default='outdated_entry') - # For the tests, the CourseOverviewFactory does a LazyAttribute on - # display_org_with_default - display_org_with_default = models.TextField() - number = models.TextField() - created = models.DateTimeField(null=True) # from TimeStampedModel - start = models.DateTimeField(null=True) - end = models.DateTimeField(null=True) - enrollment_start = models.DateTimeField(null=True) - enrollment_end = models.DateTimeField(null=True) - self_paced = models.BooleanField(default=False) - - @property - def display_name_with_default_escaped(self): - return self.display_name - - @property - def display_number_with_default(self): - return self.number - - @property - def display_order_with_default(self): - return self.org - - @classmethod - def get_from_id(cls, course_id): - return CourseOverview.objects.get(id=course_id) diff --git a/mocks/ginkgo/openedx/core/djangoapps/course_groups/migrations/0001_initial.py b/mocks/ginkgo/openedx/core/djangoapps/course_groups/migrations/0001_initial.py deleted file mode 100644 index c63d0b57..00000000 --- a/mocks/ginkgo/openedx/core/djangoapps/course_groups/migrations/0001_initial.py +++ /dev/null @@ -1,98 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.db import migrations, models -import openedx.core.djangoapps.xmodule_django.models -from django.conf import settings - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='CohortMembership', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('course_id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(max_length=255)), - ], - ), - migrations.CreateModel( - name='CourseCohort', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('assignment_type', models.CharField(default=b'manual', max_length=20, choices=[(b'random', b'Random'), (b'manual', b'Manual')])), - ], - ), - migrations.CreateModel( - name='CourseCohortsSettings', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('is_cohorted', models.BooleanField(default=False)), - ('course_id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(help_text=b'Which course are these settings associated with?', unique=True, max_length=255, db_index=True)), - ('_cohorted_discussions', models.TextField(null=True, db_column=b'cohorted_discussions', blank=True)), - ('always_cohort_inline_discussions', models.BooleanField(default=False)), - ], - ), - migrations.CreateModel( - name='CourseUserGroup', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(help_text=b'What is the name of this group? Must be unique within a course.', max_length=255)), - ('course_id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(help_text=b'Which course is this group associated with?', max_length=255, db_index=True)), - ('group_type', models.CharField(max_length=20, choices=[(b'cohort', b'Cohort')])), - ('users', models.ManyToManyField(help_text=b'Who is in this group?', related_name='course_groups', to=settings.AUTH_USER_MODEL, db_index=True)), - ], - ), - migrations.CreateModel( - name='CourseUserGroupPartitionGroup', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('partition_id', models.IntegerField(help_text=b'contains the id of a cohorted partition in this course')), - ('group_id', models.IntegerField(help_text=b'contains the id of a specific group within the cohorted partition')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('course_user_group', models.OneToOneField(to='course_groups.CourseUserGroup')), - ], - ), - migrations.CreateModel( - name='UnregisteredLearnerCohortAssignments', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('email', models.CharField(db_index=True, max_length=255, blank=True)), - ('course_id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(max_length=255)), - ('course_user_group', models.ForeignKey(to='course_groups.CourseUserGroup')), - ], - ), - migrations.AddField( - model_name='coursecohort', - name='course_user_group', - field=models.OneToOneField(related_name='cohort', to='course_groups.CourseUserGroup'), - ), - migrations.AddField( - model_name='cohortmembership', - name='course_user_group', - field=models.ForeignKey(to='course_groups.CourseUserGroup'), - ), - migrations.AddField( - model_name='cohortmembership', - name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), - ), - migrations.AlterUniqueTogether( - name='unregisteredlearnercohortassignments', - unique_together=set([('course_id', 'email')]), - ), - migrations.AlterUniqueTogether( - name='courseusergroup', - unique_together=set([('name', 'course_id')]), - ), - migrations.AlterUniqueTogether( - name='cohortmembership', - unique_together=set([('user', 'course_id')]), - ), - ] diff --git a/mocks/ginkgo/openedx/core/djangoapps/course_groups/models.py b/mocks/ginkgo/openedx/core/djangoapps/course_groups/models.py deleted file mode 100644 index a30b8b84..00000000 --- a/mocks/ginkgo/openedx/core/djangoapps/course_groups/models.py +++ /dev/null @@ -1,244 +0,0 @@ -""" -Copied and modified from openedx.core.djangoapps.course_groups.models -""" - -from __future__ import absolute_import -import json -import logging - -from django.contrib.auth.models import User -from django.core.exceptions import ValidationError -from django.db import models, transaction -from django.db.models.signals import pre_delete -from django.dispatch import receiver -from openedx.core.djangoapps.xmodule_django.models import CourseKeyField -# from util.db import outer_atomic - - -class CourseUserGroup(models.Model): - """ - This model represents groups of users in a course. Groups may have different types, - which may be treated specially. For example, a user can be in at most one cohort per - course, and cohorts are used to split up the forums by group. - """ - class Meta(object): - unique_together = (('name', 'course_id'), ) - - name = models.CharField(max_length=255, - help_text=("What is the name of this group? " - "Must be unique within a course.")) - users = models.ManyToManyField(User, db_index=True, related_name='course_groups', - help_text="Who is in this group?") - - # Note: groups associated with particular runs of a course. E.g. Fall 2012 and Spring - # 2013 versions of 6.00x will have separate groups. - course_id = CourseKeyField( - max_length=255, - db_index=True, - help_text="Which course is this group associated with?", - ) - - # For now, only have group type 'cohort', but adding a type field to support - # things like 'question_discussion', 'friends', 'off-line-class', etc - COHORT = 'cohort' # If changing this string, update it in migration 0006.forwards() as well - GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'),) - group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES) - - @classmethod - def create(cls, name, course_id, group_type=COHORT): - """ - Create a new course user group. - - Args: - name: Name of group - course_id: course id - group_type: group type - """ - return cls.objects.get_or_create( - course_id=course_id, - group_type=group_type, - name=name - ) - - def __unicode__(self): - return self.name - - -class CohortMembership(models.Model): - """Used internally to enforce our particular definition of uniqueness""" - - course_user_group = models.ForeignKey(CourseUserGroup) - user = models.ForeignKey(User) - course_id = CourseKeyField(max_length=255) - - previous_cohort = None - previous_cohort_name = None - previous_cohort_id = None - - class Meta(object): - unique_together = (('user', 'course_id'), ) - - def clean_fields(self, *args, **kwargs): - if self.course_id is None: - self.course_id = self.course_user_group.course_id - super(CohortMembership, self).clean_fields(*args, **kwargs) - - def clean(self): - if self.course_user_group.group_type != CourseUserGroup.COHORT: - raise ValidationError("CohortMembership cannot be used with CourseGroup types other than COHORT") - if self.course_user_group.course_id != self.course_id: - raise ValidationError("Non-matching course_ids provided") - - # def save(self, *args, **kwargs): - # self.full_clean(validate_unique=False) - - # # Avoid infinite recursion if creating from get_or_create() call below. - # # This block also allows middleware to use CohortMembership.get_or_create without worrying about outer_atomic - # if 'force_insert' in kwargs and kwargs['force_insert'] is True: - # with transaction.atomic(): - # self.course_user_group.users.add(self.user) - # super(CohortMembership, self).save(*args, **kwargs) - # return - - # # This block will transactionally commit updates to CohortMembership and underlying course_user_groups. - # # Note the use of outer_atomic, which guarantees that operations are committed to the database on block exit. - # # If called from a view method, that method must be marked with @transaction.non_atomic_requests. - # with outer_atomic(read_committed=True): - - # saved_membership, created = CohortMembership.objects.select_for_update().get_or_create( - # user__id=self.user.id, - # course_id=self.course_id, - # defaults={ - # 'course_user_group': self.course_user_group, - # 'user': self.user - # } - # ) - - # # If the membership was newly created, all the validation and course_user_group logic was settled - # # with a call to self.save(force_insert=True), which gets handled above. - # if created: - # return - - # if saved_membership.course_user_group == self.course_user_group: - # raise ValueError("User {user_name} already present in cohort {cohort_name}".format( - # user_name=self.user.username, - # cohort_name=self.course_user_group.name - # )) - # self.previous_cohort = saved_membership.course_user_group - # self.previous_cohort_name = saved_membership.course_user_group.name - # self.previous_cohort_id = saved_membership.course_user_group.id - # self.previous_cohort.users.remove(self.user) - - # saved_membership.course_user_group = self.course_user_group - # self.course_user_group.users.add(self.user) - - # super(CohortMembership, saved_membership).save(update_fields=['course_user_group']) - - -# Needs to exist outside class definition in order to use 'sender=CohortMembership' -# @receiver(pre_delete, sender=CohortMembership) -# def remove_user_from_cohort(sender, instance, **kwargs): # pylint: disable=unused-argument -# """ -# Ensures that when a CohortMemebrship is deleted, the underlying CourseUserGroup -# has its users list updated to reflect the change as well. -# """ -# instance.course_user_group.users.remove(instance.user) -# instance.course_user_group.save() - - -class CourseUserGroupPartitionGroup(models.Model): - """ - Create User Partition Info. - """ - course_user_group = models.OneToOneField(CourseUserGroup) - partition_id = models.IntegerField( - help_text="contains the id of a cohorted partition in this course" - ) - group_id = models.IntegerField( - help_text="contains the id of a specific group within the cohorted partition" - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - -class CourseCohortsSettings(models.Model): - """ - This model represents cohort settings for courses. - The only non-deprecated fields are `is_cohorted` and `course_id`. - """ - is_cohorted = models.BooleanField(default=False) - - course_id = CourseKeyField( - unique=True, - max_length=255, - db_index=True, - help_text="Which course are these settings associated with?", - ) - - _cohorted_discussions = models.TextField(db_column='cohorted_discussions', null=True, blank=True) # JSON list - - # Note that although a default value is specified here for always_cohort_inline_discussions (False), - # in reality the default value at the time that cohorting is enabled for a course comes from - # course_module.always_cohort_inline_discussions (via `migrate_cohort_settings`). - # pylint: disable=invalid-name - # DEPRECATED-- DO NOT USE: Instead use `CourseDiscussionSettings.always_divide_inline_discussions` - # via `get_course_discussion_settings` or `set_course_discussion_settings`. - always_cohort_inline_discussions = models.BooleanField(default=False) - - @property - def cohorted_discussions(self): - """ - DEPRECATED-- DO NOT USE. Instead use `CourseDiscussionSettings.divided_discussions` - via `get_course_discussion_settings`. - """ - return json.loads(self._cohorted_discussions) - - @cohorted_discussions.setter - def cohorted_discussions(self, value): - """ - DEPRECATED-- DO NOT USE. Instead use `CourseDiscussionSettings` via `set_course_discussion_settings`. - """ - self._cohorted_discussions = json.dumps(value) - - -class CourseCohort(models.Model): - """ - This model represents cohort related info. - """ - course_user_group = models.OneToOneField(CourseUserGroup, unique=True, related_name='cohort') - - RANDOM = 'random' - MANUAL = 'manual' - ASSIGNMENT_TYPE_CHOICES = ((RANDOM, 'Random'), (MANUAL, 'Manual'),) - assignment_type = models.CharField(max_length=20, choices=ASSIGNMENT_TYPE_CHOICES, default=MANUAL) - - @classmethod - def create(cls, cohort_name=None, course_id=None, course_user_group=None, assignment_type=MANUAL): - """ - Create a complete(CourseUserGroup + CourseCohort) object. - - Args: - cohort_name: Name of the cohort to be created - course_id: Course Id - course_user_group: CourseUserGroup - assignment_type: 'random' or 'manual' - """ - if course_user_group is None: - course_user_group, __ = CourseUserGroup.create(cohort_name, course_id) - - course_cohort, __ = cls.objects.get_or_create( - course_user_group=course_user_group, - defaults={'assignment_type': assignment_type} - ) - - return course_cohort - - -class UnregisteredLearnerCohortAssignments(models.Model): - - class Meta(object): - unique_together = (('course_id', 'email'), ) - - course_user_group = models.ForeignKey(CourseUserGroup) - email = models.CharField(blank=True, max_length=255, db_index=True) - course_id = CourseKeyField(max_length=255) diff --git a/mocks/ginkgo/openedx/core/djangoapps/xmodule_django/models.py b/mocks/ginkgo/openedx/core/djangoapps/xmodule_django/models.py deleted file mode 100644 index 47c80e36..00000000 --- a/mocks/ginkgo/openedx/core/djangoapps/xmodule_django/models.py +++ /dev/null @@ -1,143 +0,0 @@ -'''Provides edx-platform functionality for Figures tests - -Provides Opaque key derived classes to support edx-platform mocks that use -CourseKeyField objects - -This is to make sure that course key related type errors are trapped in -figures tests:: - - "course-v1:MyOrg EdX101 2015_Spring" is not an instance of - - - -Code copied and modified as needed from: - -https://github.com/edx/edx-platform/blob/open-release/ginkgo.master/openedx/core/djangoapps/xmodule_django/models.py - -''' - -from __future__ import absolute_import -from django.db import models -from opaque_keys.edx.keys import CourseKey -import six - -def _strip_object(key): - """ - Strips branch and version info if the given key supports those attributes. - """ - if hasattr(key, 'version_agnostic') and hasattr(key, 'for_branch'): - return key.for_branch(None).version_agnostic() - else: - return key - - -def _strip_value(value, lookup='exact'): - """ - Helper function to remove the branch and version information from the given value, - which could be a single object or a list. - """ - if lookup == 'in': - stripped_value = [_strip_object(el) for el in value] - else: - stripped_value = _strip_object(value) - return stripped_value - - -class OpaqueKeyField(six.with_metaclass(models.SubfieldBase, models.CharField)): - """ - A django field for storing OpaqueKeys. - - The baseclass will return the value from the database as a string, rather than an instance - of an OpaqueKey, leaving the application to determine which key subtype to parse the string - as. - - Subclasses must specify a KEY_CLASS attribute, in which case the field will use :meth:`from_string` - to parse the key string, and will return an instance of KEY_CLASS. - """ - description = "An OpaqueKey object, saved to the DB in the form of a string." - - Empty = object() - KEY_CLASS = None - - def __init__(self, *args, **kwargs): - if self.KEY_CLASS is None: - raise ValueError('Must specify KEY_CLASS in OpaqueKeyField subclasses') - - super(OpaqueKeyField, self).__init__(*args, **kwargs) - - def to_python(self, value): - if value is self.Empty or value is None: - return None - - assert isinstance(value, (six.string_types, self.KEY_CLASS)), \ - "%s is not an instance of basestring or %s" % (value, self.KEY_CLASS) - if value == '': - # handle empty string for models being created w/o fields populated - return None - - if isinstance(value, six.string_types): - if value.endswith('\n'): - # An opaque key with a trailing newline has leaked into the DB. - # Log and strip the value. - log.warning(u'{}:{}:{}:to_python: Invalid key: {}. Removing trailing newline.'.format( - self.model._meta.db_table, # pylint: disable=protected-access - self.name, - self.KEY_CLASS.__name__, - repr(value) - )) - value = value.rstrip() - return self.KEY_CLASS.from_string(value) - else: - return value - - def get_prep_lookup(self, lookup, value): - if lookup == 'isnull': - raise TypeError('Use {0}.Empty rather than None to query for a missing {0}'.format(self.__class__.__name__)) - - return super(OpaqueKeyField, self).get_prep_lookup( - lookup, - # strip key before comparing - _strip_value(value, lookup) - ) - - def get_prep_value(self, value): - if value is self.Empty or value is None: - return '' # CharFields should use '' as their empty value, rather than None - - # HACK: Remarking out the assertion until we can investigate why - # the value is sometimes unicode and sometimes an OpaqueKey/CourseLocator - #assert isinstance(value, self.KEY_CLASS), "%s is not an instance of %s" % (value, self.KEY_CLASS) - serialized_key = six.text_type(_strip_value(value)) - if serialized_key.endswith('\n'): - # An opaque key object serialized to a string with a trailing newline. - # Log the value - but do not modify it. - log.warning(u'{}:{}:{}:get_prep_value: Invalid key: {}.'.format( - self.model._meta.db_table, # pylint: disable=protected-access - self.name, - self.KEY_CLASS.__name__, - repr(serialized_key) - )) - return serialized_key - - def validate(self, value, model_instance): - """Validate Empty values, otherwise defer to the parent""" - # raise validation error if the use of this field says it can't be blank but it is - if not self.blank and value is self.Empty: - raise ValidationError(self.error_messages['blank']) - else: - return super(OpaqueKeyField, self).validate(value, model_instance) - - def run_validators(self, value): - """Validate Empty values, otherwise defer to the parent""" - if value is self.Empty: - return - - return super(OpaqueKeyField, self).run_validators(value) - - -class CourseKeyField(OpaqueKeyField): - """ - A django Field that stores a CourseKey object as a string. - """ - description = "A CourseKey object, saved to the DB in the form of a string" - KEY_CLASS = CourseKey diff --git a/mocks/ginkgo/student/migrations/0001_initial.py b/mocks/ginkgo/student/migrations/0001_initial.py deleted file mode 100644 index 7d2cfa70..00000000 --- a/mocks/ginkgo/student/migrations/0001_initial.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.db import migrations, models -import django_countries.fields -from django.conf import settings -import openedx.core.djangoapps.xmodule_django.models - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='CourseAccessRole', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('org', models.CharField(db_index=True, max_length=64, blank=True)), - ('course_id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(db_index=True, max_length=255, blank=True)), - ('role', models.CharField(max_length=64, db_index=True)), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='CourseEnrollment', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('course_id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(max_length=255, db_index=True)), - ('created', models.DateTimeField(null=True)), - ('is_active', models.BooleanField(default=True)), - ('mode', models.CharField(default=b'audit', max_length=100)), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ('user', 'course_id'), - }, - ), - migrations.CreateModel( - name='UserProfile', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(db_index=True, max_length=255, blank=True)), - ('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)), - ('year_of_birth', models.IntegerField(db_index=True, null=True, blank=True)), - ('gender', models.CharField(blank=True, max_length=6, null=True, db_index=True, choices=[(b'm', b'Male'), (b'f', b'Female'), (b'o', b'Other/Prefer Not to Say')])), - ('level_of_education', models.CharField(blank=True, max_length=6, null=True, db_index=True, choices=[(b'p', b'Doctorate'), (b'm', b"Master's or professional degree"), (b'b', b"Bachelor's degree"), (b'a', b'Associate degree'), (b'hs', b'Secondary/high school'), (b'jhs', b'Junior secondary/junior high/middle school'), (b'el', b'Elementary/primary school'), (b'none', b'No formal education'), (b'other', b'Other education')])), - ('profile_image_uploaded_at', models.DateTimeField(null=True, blank=True)), - ('user', models.OneToOneField(related_name='profile', to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.AlterUniqueTogether( - name='courseenrollment', - unique_together=set([('user', 'course_id')]), - ), - migrations.AlterUniqueTogether( - name='courseaccessrole', - unique_together=set([('user', 'org', 'course_id', 'role')]), - ), - ] diff --git a/mocks/ginkgo/student/migrations/0002_auto_20191231_1006.py b/mocks/ginkgo/student/migrations/0002_auto_20191231_1006.py deleted file mode 100644 index df6a9ed5..00000000 --- a/mocks/ginkgo/student/migrations/0002_auto_20191231_1006.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('student', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='userprofile', - name='allow_certificate', - field=models.BooleanField(default=1), - ), - migrations.AddField( - model_name='userprofile', - name='bio', - field=models.CharField(max_length=3000, null=True, blank=True), - ), - migrations.AddField( - model_name='userprofile', - name='city', - field=models.TextField(null=True, blank=True), - ), - migrations.AddField( - model_name='userprofile', - name='goals', - field=models.TextField(null=True, blank=True), - ), - migrations.AddField( - model_name='userprofile', - name='mailing_address', - field=models.TextField(null=True, blank=True), - ), - ] diff --git a/mocks/ginkgo/student/models.py b/mocks/ginkgo/student/models.py deleted file mode 100644 index 5a1d81b9..00000000 --- a/mocks/ginkgo/student/models.py +++ /dev/null @@ -1,178 +0,0 @@ - -from __future__ import absolute_import -from collections import defaultdict -from datetime import datetime - -from pytz import UTC - -from django.db import models -from django.contrib.auth.models import User -from django.utils.translation import ugettext_noop - -from django_countries.fields import CountryField - -from course_modes.models import CourseMode - -from openedx.core.djangoapps.content.course_overviews.models import ( - CourseOverview, -) -from openedx.core.djangoapps.xmodule_django.models import CourseKeyField -from six.moves import range - -class UserProfile(models.Model): - ''' - The production model is student.models.UserProfile - ''' - user = models.OneToOneField(User, unique=True, db_index=True, related_name='profile') - name = models.CharField(blank=True, max_length=255, db_index=True) - country = CountryField(blank=True, null=True) - - # Optional demographic data we started capturing from Fall 2012 - this_year = datetime.now(UTC).year - VALID_YEARS = list(range(this_year, this_year - 120, -1)) - year_of_birth = models.IntegerField(blank=True, null=True, db_index=True) - - GENDER_CHOICES = ( - ('m', ugettext_noop('Male')), - ('f', ugettext_noop('Female')), - # Translators: 'Other' refers to the student's gender - ('o', ugettext_noop('Other/Prefer Not to Say')) - ) - gender = models.CharField( - blank=True, null=True, max_length=6, db_index=True, choices=GENDER_CHOICES - ) - - # [03/21/2013] removed these, but leaving comment since there'll still be - # p_se and p_oth in the existing data in db. - # ('p_se', 'Doctorate in science or engineering'), - # ('p_oth', 'Doctorate in another field'), - LEVEL_OF_EDUCATION_CHOICES = ( - ('p', ugettext_noop('Doctorate')), - ('m', ugettext_noop("Master's or professional degree")), - ('b', ugettext_noop("Bachelor's degree")), - ('a', ugettext_noop("Associate degree")), - ('hs', ugettext_noop("Secondary/high school")), - ('jhs', ugettext_noop("Junior secondary/junior high/middle school")), - ('el', ugettext_noop("Elementary/primary school")), - # Translators: 'None' refers to the student's level of education - ('none', ugettext_noop("No formal education")), - # Translators: 'Other' refers to the student's level of education - ('other', ugettext_noop("Other education")) - ) - level_of_education = models.CharField( - blank=True, null=True, max_length=6, db_index=True, - choices=LEVEL_OF_EDUCATION_CHOICES - ) - mailing_address = models.TextField(blank=True, null=True) - city = models.TextField(blank=True, null=True) - country = CountryField(blank=True, null=True) - goals = models.TextField(blank=True, null=True) - allow_certificate = models.BooleanField(default=1) - bio = models.CharField(blank=True, null=True, max_length=3000, db_index=False) - profile_image_uploaded_at = models.DateTimeField(null=True, blank=True) - - @property - def has_profile_image(self): - """ - Convenience method that returns a boolean indicating whether or not - this user has uploaded a profile image. - """ - return self.profile_image_uploaded_at is not None - - -class CourseEnrollmentManager(models.Manager): - def enrollment_counts(self, course_id): - - query = super(CourseEnrollmentManager, self).get_queryset().filter( - course_id=course_id, is_active=True).values( - 'mode').order_by().annotate(models.Count('mode')) - total = 0 - enroll_dict = defaultdict(int) - for item in query: - enroll_dict[item['mode']] = item['mode__count'] - total += item['mode__count'] - enroll_dict['total'] = total - return enroll_dict - - -class CourseEnrollment(models.Model): - ''' - The production model is student.models.CourseEnrollment - - The purpose of this mock is to provide the model needed to - retrieve: - * The learners enrolled in a course - * When a learner enrolled - * If the learner is active - ''' - - user = models.ForeignKey(User) - course_id = CourseKeyField(max_length=255, db_index=True) - created = models.DateTimeField(null=True) - - # If is_active is False, then the student is not considered to be enrolled - # in the course (is_enrolled() will return False) - is_active = models.BooleanField(default=True) - - mode = models.CharField(default=CourseMode.DEFAULT_MODE_SLUG, max_length=100) - - objects = CourseEnrollmentManager() - - - class Meta(object): - unique_together = (('user', 'course_id'),) - ordering = ('user', 'course_id') - - def __init__(self, *args, **kwargs): - super(CourseEnrollment, self).__init__(*args, **kwargs) - - # Private variable for storing course_overview to minimize calls to the database. - # When the property .course_overview is accessed for the first time, this variable will be set. - self._course_overview = None - - @property - def course_overview(self): - if not self._course_overview: - try: - self._course_overview = CourseOverview.get_from_id(self.course_id) - except (CourseOverview.DoesNotExist, IOError): - self._course_overview = None - return self._course_overview - -class CourseAccessRole(models.Model): - user = models.ForeignKey(User) - # blank org is for global group based roles such as course creator (may be deprecated) - org = models.CharField(max_length=64, db_index=True, blank=True) - # blank course_id implies org wide role - course_id = CourseKeyField(max_length=255, db_index=True, blank=True) - role = models.CharField(max_length=64, db_index=True) - - class Meta(object): - unique_together = ('user', 'org', 'course_id', 'role') - - @property - def _key(self): - """ - convenience function to make eq overrides easier and clearer. arbitrary decision - that role is primary, followed by org, course, and then user - """ - return (self.role, self.org, self.course_id, self.user_id) - - def __eq__(self, other): - """ - Overriding eq b/c the django impl relies on the primary key which requires fetch. sometimes we - just want to compare roles w/o doing another fetch. - """ - return type(self) == type(other) and self._key == other._key # pylint: disable=protected-access - - def __hash__(self): - return hash(self._key) - - def __lt__(self, other): - """ - Lexigraphic sort - """ - return self._key < other._key # pylint: disable=protected-access - - def __unicode__(self): - return "[CourseAccessRole] user: {} role: {} org: {} course: {}".format(self.user.username, self.role, self.org, self.course_id) diff --git a/mocks/ginkgo/student/roles.py b/mocks/ginkgo/student/roles.py deleted file mode 100644 index e2f776b9..00000000 --- a/mocks/ginkgo/student/roles.py +++ /dev/null @@ -1,60 +0,0 @@ -''' -Mocks role classes needed in Figures tests -''' - -from __future__ import absolute_import -from django.contrib.auth.models import User - -from openedx.core.djangoapps.xmodule_django.models import CourseKeyField -from student.models import CourseAccessRole - -class MockCourseRole(object): - ''' - Mock student.models.CourseRole and its parent classes - - Guideline: only implement the minimum needed to simulate edx-platform for - the Figures unit tests - ''' - def __init__(self, role, course_key): - - # The following are declared in studen.roles.RoleBase - self.org = '' - self._role_name = role - # The following are declared in student.roles.CourseRole - self.role = role - self.course_key = course_key - - def users_with_role(self): - """ - Return a django QuerySet for all of the users with this role - """ - # Org roles don't query by CourseKey, so use CourseKeyField.Empty for that query - if self.course_key is None: - self.course_key = CourseKeyField.Empty - entries = User.objects.filter( - courseaccessrole__role=self._role_name, - courseaccessrole__org=self.org, - courseaccessrole__course_id=self.course_key - ) - return entries - - -class CourseCcxCoachRole(MockCourseRole): - ROLE = 'ccx_coach' - - def __init__(self, *args, **kwargs): - super(CourseCcxCoachRole, self).__init__(self.ROLE, *args, **kwargs) - - -class CourseInstructorRole(MockCourseRole): - ROLE = 'instructor' - - def __init__(self, *args, **kwargs): - super(CourseInstructorRole, self).__init__(self.ROLE, *args, **kwargs) - - -class CourseStaffRole(MockCourseRole): - ROLE = 'staff' - - def __init__(self, *args, **kwargs): - super(CourseStaffRole, self).__init__(self.ROLE, *args, **kwargs) diff --git a/mocks/ginkgo/xmodule/modulestore/django.py b/mocks/ginkgo/xmodule/modulestore/django.py deleted file mode 100644 index 5ee7b6bb..00000000 --- a/mocks/ginkgo/xmodule/modulestore/django.py +++ /dev/null @@ -1,72 +0,0 @@ -''' - -./common/lib/xmodule/xmodule/modulestore/django.py -''' - -from __future__ import absolute_import -from contextlib import contextmanager - - -class MockCourse(object): - ''' - This is a mock of the CourseDescriptor - - This usually seems to be a 'xblock.internal.CourseDescriptorWithMixins' - object. Which appears to be an ephemeral class (yeah, let that sink in). - which makes working with it a real joy since it also appears to not be - documented anywhere, even though it might be one of the most important - classes in Open edX. - - There is a 'CourseDescriptor' class in - ``common/lib/xmodule/xmodule/course_module.py`` - - So this mock is a minimal attempt - ''' - note = 'I am a mock course object.' - - def __init__(self, course_locator, **kwargs): - self.grading_policy = None - self.id = course_locator - def set_grading_policy(self, grading_policy): - self.grading_policy = grading_policy - - -class MockMixedModulestore(object): - ''' - We are mocking functionalit needed by courseware.courses - ''' - def __init__(self, **kwargs): - pass - - @contextmanager - def bulk_operations(self, course_id, emit_signals=True, ignore_Case=False): - ''' - see xmodule/modulestore/__init__.py - - Bulk operations look like a likely candidate to extract from edx-platform - - For now, we're doing the absolute minimum mocking - ''' - yield - - def get_course(self, course_id, depth): - return MockCourse(course_locator=course_id) - -def modulestore(): - ''' - Need to mock: - - with modulestore().bulk_operations(course_key): - course = modulestore().get_course(course_key, depth=depth) - ''' - - # This is the production code: - # We are skipping mocking the CCX modulestore - # _MIXED_MODULESTORE = create_modulestore_instance( - # settings.MODULESTORE['default']['ENGINE'], - # contentstore(), - # settings.MODULESTORE['default'].get('DOC_STORE_CONFIG', {}), - # settings.MODULESTORE['default'].get('OPTIONS', {}) - # ) - - return MockMixedModulestore() diff --git a/mocks/hawthorn/course_modes/models.py b/mocks/hawthorn/course_modes/models.py deleted file mode 100644 index 458d60e5..00000000 --- a/mocks/hawthorn/course_modes/models.py +++ /dev/null @@ -1,13 +0,0 @@ - - - -class CourseMode(object): - ''' - In edx-platform, this is a Django Model. - - Currently we only need to mock getting the - default mode slug - ''' - - # - DEFAULT_MODE_SLUG = 'audit' \ No newline at end of file diff --git a/mocks/hawthorn/courseware/courses.py b/mocks/hawthorn/courseware/courses.py deleted file mode 100644 index 59dd5a8a..00000000 --- a/mocks/hawthorn/courseware/courses.py +++ /dev/null @@ -1,42 +0,0 @@ -''' - -./common/lib/xmodule/xmodule/modulestore -''' - -from __future__ import absolute_import -from django.http import Http404 -from xmodule.modulestore.django import modulestore -import six - - -def get_course(course_id, depth=0): - """ - Given a course id, return the corresponding course descriptor. - - If the course does not exist, raises a ValueError. This is appropriate - for internal use. - - depth: The number of levels of children for the modulestore to cache. - None means infinite depth. Default is to fetch no children. - """ - course = modulestore().get_course(course_id, depth=depth) - if course is None: - raise ValueError(u"Course not found: {0}".format(course_id)) - return course - - -def get_course_by_id(course_key, depth=0): - """ - Given a course id, return the corresponding course descriptor. - - If such a course does not exist, raises a 404. - - depth: The number of levels of children for the modulestore to cache. None means infinite depth - """ - with modulestore().bulk_operations(course_key): - course = modulestore().get_course(course_key, depth=depth) - - if course: - return course - else: - raise Http404("Course not found: {}.".format(six.text_type(course_key))) diff --git a/mocks/hawthorn/courseware/migrations/0001_initial.py b/mocks/hawthorn/courseware/migrations/0001_initial.py deleted file mode 100644 index b496afba..00000000 --- a/mocks/hawthorn/courseware/migrations/0001_initial.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.15 on 2019-07-26 02:20 -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import openedx.core.djangoapps.xmodule_django.models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='StudentModule', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('course_id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(db_index=True, max_length=255)), - ('created', models.DateTimeField()), - ('modified', models.DateTimeField()), - ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/mocks/hawthorn/courseware/migrations/__init__.py b/mocks/hawthorn/courseware/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/courseware/models.py b/mocks/hawthorn/courseware/models.py deleted file mode 100644 index 5f9b6e04..00000000 --- a/mocks/hawthorn/courseware/models.py +++ /dev/null @@ -1,59 +0,0 @@ - -from __future__ import absolute_import -from django.db import models -from django.contrib.auth.models import User - -from openedx.core.djangoapps.xmodule_django.models import ( - CourseKeyField, - #! LocationKeyField, - ) - - -class StudentModule(models.Model): - '''Mocks the courseware.models.StudentModule - - class attributes declared in StudentModule but not yet - needed for mocking are remarked out with a '#!' - They are here to - A) Help understand context of the class without requiring opening the - courseware/models.py file - B) Be available to quickly update this mock when needed - ''' - #! MODEL_TAGS = ['course_id', 'module_type'] - - # For a homework problem, contains a JSON - # object consisting of state - #! MODULE_TYPES = (('problem', 'problem'), - #! ('video', 'video'), - #! ('html', 'html'), - #! ('course', 'course'), - #! ('chapter', 'Section'), - #! ('sequential', 'Subsection'), - #! ('library_content', 'Library Content')) - - #! module_state_key = LocationKeyField(max_length=255, db_index=True, db_column='module_id') - - student = models.ForeignKey(User, db_index=True, on_delete=models.CASCADE,) - - course_id = CourseKeyField(max_length=255, db_index=True) - - #! class Meta(object): - #! app_label = "courseware" - #! unique_together = (('student', 'module_state_key', 'course_id'),) - - #! # Internal state of the object - #! state = models.TextField(null=True, blank=True) - - #! # Grade, and are we done? - #! grade = models.FloatField(null=True, blank=True, db_index=True) - #! max_grade = models.FloatField(null=True, blank=True) - #! DONE_TYPES = ( - #! ('na', 'NOT_APPLICABLE'), - #! ('f', 'FINISHED'), - #! ('i', 'INCOMPLETE'), - #! ) - #! done = models.CharField(max_length=8, choices=DONE_TYPES, default='na', db_index=True) - - # the production model sets 'auto_now_add=True' andn 'db_index=True' - created = models.DateTimeField() - modified = models.DateTimeField() diff --git a/mocks/hawthorn/lms/__init__.py b/mocks/hawthorn/lms/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/lms/djangoapps/__init__.py b/mocks/hawthorn/lms/djangoapps/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/lms/djangoapps/certificates/__init__.py b/mocks/hawthorn/lms/djangoapps/certificates/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/lms/djangoapps/certificates/migrations/0001_initial.py b/mocks/hawthorn/lms/djangoapps/certificates/migrations/0001_initial.py deleted file mode 100644 index e5e8e7e9..00000000 --- a/mocks/hawthorn/lms/djangoapps/certificates/migrations/0001_initial.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.15 on 2019-07-26 02:19 -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import openedx.core.djangoapps.xmodule_django.models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='GeneratedCertificate', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('course_id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(blank=True, default=None, max_length=255)), - ('created_date', models.DateTimeField()), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/mocks/hawthorn/lms/djangoapps/certificates/migrations/__init__.py b/mocks/hawthorn/lms/djangoapps/certificates/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/lms/djangoapps/certificates/models.py b/mocks/hawthorn/lms/djangoapps/certificates/models.py deleted file mode 100644 index 669c52f0..00000000 --- a/mocks/hawthorn/lms/djangoapps/certificates/models.py +++ /dev/null @@ -1,11 +0,0 @@ - -from __future__ import absolute_import -from django.db import models -from django.contrib.auth.models import User - -from openedx.core.djangoapps.xmodule_django.models import CourseKeyField - -class GeneratedCertificate(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE,) - course_id = CourseKeyField(max_length=255, blank=True, default=None) - created_date = models.DateTimeField() diff --git a/mocks/hawthorn/lms/djangoapps/grades/__init__.py b/mocks/hawthorn/lms/djangoapps/grades/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/lms/djangoapps/grades/course_grade_factory.py b/mocks/hawthorn/lms/djangoapps/grades/course_grade_factory.py deleted file mode 100644 index 362a6d7f..00000000 --- a/mocks/hawthorn/lms/djangoapps/grades/course_grade_factory.py +++ /dev/null @@ -1,26 +0,0 @@ - - -from __future__ import absolute_import -from lms.djangoapps.grades.course_grade import CourseGrade - - -class MockCourseData(object): - - def __init__(self, user, course=None, collected_block_structure=None, structure=None, course_key=None): - if not any([course, collected_block_structure, structure, course_key]): - raise ValueError( - "You must specify one of course, collected_block_structure, structure, or course_key to this method." - ) - self.user = user - self._collected_block_structure = collected_block_structure - self._structure = structure - self._course = course - self._course_key = course_key - self._location = None - - -class CourseGradeFactory(object): - - def read(self, user, course=None, collected_block_structure=None, course_structure=None, course_key=None): - course_data = MockCourseData(user, course, collected_block_structure, course_structure, course_key) - return CourseGrade(user, course_data, force_update_subsections=False) diff --git a/mocks/hawthorn/lms/djangoapps/teams/__init__.py b/mocks/hawthorn/lms/djangoapps/teams/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/lms/djangoapps/teams/models.py b/mocks/hawthorn/lms/djangoapps/teams/models.py deleted file mode 100644 index eeb0c8f2..00000000 --- a/mocks/hawthorn/lms/djangoapps/teams/models.py +++ /dev/null @@ -1,23 +0,0 @@ - -from __future__ import absolute_import -from django.db import models -from django.contrib.auth.models import User - -class CourseTeam(models.Model): - - class Meta: - app_label = 'teams' - - name = models.CharField(max_length=255, db_index=True) - users = models.ManyToManyField(User, - db_index=True, related_name='teams', through='CourseTeamMembership') - -class CourseTeamMembership(models.Model): - - class Meta: - app_label = "teams" - unique_together = (('user', 'team'),) - - user = models.ForeignKey(User, on_delete=models.CASCADE,) - team = models.ForeignKey(CourseTeam, related_name='membership',on_delete=models.CASCADE,) - diff --git a/mocks/hawthorn/openedx/__init__.py b/mocks/hawthorn/openedx/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/openedx/core/__init__.py b/mocks/hawthorn/openedx/core/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/openedx/core/djangoapps/__init__.py b/mocks/hawthorn/openedx/core/djangoapps/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/openedx/core/djangoapps/content/__init__.py b/mocks/hawthorn/openedx/core/djangoapps/content/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/openedx/core/djangoapps/content/course_overviews/__init__.py b/mocks/hawthorn/openedx/core/djangoapps/content/course_overviews/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/openedx/core/djangoapps/content/course_overviews/migrations/0001_initial.py b/mocks/hawthorn/openedx/core/djangoapps/content/course_overviews/migrations/0001_initial.py deleted file mode 100644 index a4bf660a..00000000 --- a/mocks/hawthorn/openedx/core/djangoapps/content/course_overviews/migrations/0001_initial.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.15 on 2019-07-26 02:20 -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.db import migrations, models -import openedx.core.djangoapps.xmodule_django.models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='CourseOverview', - fields=[ - ('version', models.IntegerField()), - ('id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(db_index=True, max_length=255, primary_key=True, serialize=False)), - ('display_name', models.TextField(null=True)), - ('org', models.TextField(default=b'outdated_entry', max_length=255)), - ('display_org_with_default', models.TextField()), - ('number', models.TextField()), - ('created', models.DateTimeField(null=True)), - ('start', models.DateTimeField(null=True)), - ('end', models.DateTimeField(null=True)), - ('enrollment_start', models.DateTimeField(null=True)), - ('enrollment_end', models.DateTimeField(null=True)), - ('self_paced', models.BooleanField(default=False)), - ], - ), - ] diff --git a/mocks/hawthorn/openedx/core/djangoapps/content/course_overviews/migrations/__init__.py b/mocks/hawthorn/openedx/core/djangoapps/content/course_overviews/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/openedx/core/djangoapps/content/course_overviews/models.py b/mocks/hawthorn/openedx/core/djangoapps/content/course_overviews/models.py deleted file mode 100644 index 43f6bd07..00000000 --- a/mocks/hawthorn/openedx/core/djangoapps/content/course_overviews/models.py +++ /dev/null @@ -1,89 +0,0 @@ -''' -Provides fake models for openedx.core.djangoapps.content.course_overviews - -Overview --------- - -The purpose of this module is to provide the minimum models in order to mock -Figures access to edx-platform models - -Reference ---------- - -See also the lms.djangoapps.course_api.serializers.CourseSerializer class - -This provides the data that are returned in the built-in edx-platform -course_api REST API calls - -Future ------- - -If and when openedx.core is re-architected to be independent from edx-platform, -or we can selectively include apps from edx-platform without requiring complex -test settings and a filesystem based test infrastructure, then we can revisit -removing these mocks - -''' - -from __future__ import absolute_import -from django.db import models - -from openedx.core.djangoapps.xmodule_django.models import CourseKeyField - -from figures.helpers import as_course_key - - -class CourseOverview(models.Model): - ''' - Provides a mock model for the edx-platform 'CourseOverview' model - - Future Improvements - ------------------- - - We want to provide enhanced live querying like - - "Which courses are invitation only?" - - "Which courses have a maximum allowed enrollment above X" - - ''' - # Faking id, picking arbitrary length - # Actual field is of type opaque_keys.edx.keys.CourseKey - #id = models.CharField(db_index=True, primary_key=True, max_length=255) - - class Meta(object): - app_label = 'course_overviews' - - # IMPORTANT: Bump this whenever you modify this model and/or add a migration. - VERSION = 6 - - # Cache entry versioning. - version = models.IntegerField() - - id = CourseKeyField(db_index=True, primary_key=True, max_length=255) - display_name = models.TextField(null=True) - org = models.TextField(max_length=255, default='outdated_entry') - # For the tests, the CourseOverviewFactory does a LazyAttribute on - # display_org_with_default - display_org_with_default = models.TextField() - number = models.TextField() - created = models.DateTimeField(null=True) # from TimeStampedModel - start = models.DateTimeField(null=True) - end = models.DateTimeField(null=True) - enrollment_start = models.DateTimeField(null=True) - enrollment_end = models.DateTimeField(null=True) - self_paced = models.BooleanField(default=False) - - @property - def display_name_with_default_escaped(self): - return self.display_name - - @property - def display_number_with_default(self): - return self.number - - @property - def display_order_with_default(self): - return self.org - - @classmethod - def get_from_id(cls, course_id): - return cls.objects.get(id=as_course_key(course_id)) diff --git a/mocks/hawthorn/openedx/core/djangoapps/course_groups/__init__.py b/mocks/hawthorn/openedx/core/djangoapps/course_groups/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/openedx/core/djangoapps/course_groups/migrations/__init__.py b/mocks/hawthorn/openedx/core/djangoapps/course_groups/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/openedx/core/djangoapps/course_groups/models.py b/mocks/hawthorn/openedx/core/djangoapps/course_groups/models.py deleted file mode 100644 index 725ba1c2..00000000 --- a/mocks/hawthorn/openedx/core/djangoapps/course_groups/models.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -Copied and modified from openedx.core.djangoapps.course_groups.models -""" - -from __future__ import absolute_import -from django.contrib.auth.models import User -from django.core.exceptions import ValidationError -from django.db import models, transaction - - -from opaque_keys.edx.django.models import CourseKeyField - - - -class CourseUserGroup(models.Model): - """ - This model represents groups of users in a course. Groups may have different types, - which may be treated specially. For example, a user can be in at most one cohort per - course, and cohorts are used to split up the forums by group. - """ - class Meta(object): - unique_together = (('name', 'course_id'), ) - - name = models.CharField(max_length=255, - help_text=("What is the name of this group? " - "Must be unique within a course.")) - users = models.ManyToManyField(User, db_index=True, related_name='course_groups', - help_text="Who is in this group?") - - # Note: groups associated with particular runs of a course. E.g. Fall 2012 and Spring - # 2013 versions of 6.00x will have separate groups. - course_id = CourseKeyField( - max_length=255, - db_index=True, - help_text="Which course is this group associated with?", - ) - - # For now, only have group type 'cohort', but adding a type field to support - # things like 'question_discussion', 'friends', 'off-line-class', etc - COHORT = 'cohort' # If changing this string, update it in migration 0006.forwards() as well - GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'),) - group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES) - - @classmethod - def create(cls, name, course_id, group_type=COHORT): - """ - Create a new course user group. - - Args: - name: Name of group - course_id: course id - group_type: group type - """ - return cls.objects.get_or_create( - course_id=course_id, - group_type=group_type, - name=name - ) - - def __unicode__(self): - return self.name - - -class CohortMembership(models.Model): - """Used internally to enforce our particular definition of uniqueness""" - - course_user_group = models.ForeignKey(CourseUserGroup, on_delete=models.CASCADE) - user = models.ForeignKey(User, on_delete=models.CASCADE) - course_id = CourseKeyField(max_length=255) - - previous_cohort = None - previous_cohort_name = None - previous_cohort_id = None - - class Meta(object): - unique_together = (('user', 'course_id'), ) - - def clean_fields(self, *args, **kwargs): - if self.course_id is None: - self.course_id = self.course_user_group.course_id - super(CohortMembership, self).clean_fields(*args, **kwargs) - - def clean(self): - if self.course_user_group.group_type != CourseUserGroup.COHORT: - raise ValidationError("CohortMembership cannot be used with CourseGroup types other than COHORT") - if self.course_user_group.course_id != self.course_id: - raise ValidationError("Non-matching course_ids provided") - - def save(self, *args, **kwargs): - self.full_clean(validate_unique=False) - - # Avoid infinite recursion if creating from get_or_create() call below. - # This block also allows middleware to use CohortMembership.get_or_create without worrying about outer_atomic - if 'force_insert' in kwargs and kwargs['force_insert'] is True: - with transaction.atomic(): - self.course_user_group.users.add(self.user) - super(CohortMembership, self).save(*args, **kwargs) - return - - # This block will transactionally commit updates to CohortMembership and underlying course_user_groups. - # Note the use of outer_atomic, which guarantees that operations are committed to the database on block exit. - # If called from a view method, that method must be marked with @transaction.non_atomic_requests. - with outer_atomic(read_committed=True): - - saved_membership, created = CohortMembership.objects.select_for_update().get_or_create( - user__id=self.user.id, - course_id=self.course_id, - defaults={ - 'course_user_group': self.course_user_group, - 'user': self.user - } - ) - - # If the membership was newly created, all the validation and course_user_group logic was settled - # with a call to self.save(force_insert=True), which gets handled above. - if created: - return - - if saved_membership.course_user_group == self.course_user_group: - raise ValueError("User {user_name} already present in cohort {cohort_name}".format( - user_name=self.user.username, - cohort_name=self.course_user_group.name - )) - self.previous_cohort = saved_membership.course_user_group - self.previous_cohort_name = saved_membership.course_user_group.name - self.previous_cohort_id = saved_membership.course_user_group.id - self.previous_cohort.users.remove(self.user) - - saved_membership.course_user_group = self.course_user_group - self.course_user_group.users.add(self.user) - - super(CohortMembership, saved_membership).save(update_fields=['course_user_group']) diff --git a/mocks/hawthorn/openedx/core/djangoapps/plugins/__init__.py b/mocks/hawthorn/openedx/core/djangoapps/plugins/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/openedx/core/djangoapps/plugins/constants.py b/mocks/hawthorn/openedx/core/djangoapps/plugins/constants.py deleted file mode 100644 index e9acdb37..00000000 --- a/mocks/hawthorn/openedx/core/djangoapps/plugins/constants.py +++ /dev/null @@ -1,78 +0,0 @@ -# Name of the class attribute to put in the AppConfig class of the Plugin App. -PLUGIN_APP_CLASS_ATTRIBUTE_NAME = u'plugin_app' - - -# Name of the function that belongs in the plugin Django app's settings file. -# The function should be defined as: -# def plugin_settings(settings): -# # enter code that should be injected into the given settings module. -PLUGIN_APP_SETTINGS_FUNC_NAME = u'plugin_settings' - - -class ProjectType(object): - """ - The ProjectType enum defines the possible values for the Django Projects - that are available in the edx-platform. Plugin apps use these values to - declare explicitly which projects they are extending. - """ - LMS = u'lms.djangoapp' - CMS = u'cms.djangoapp' - - -class SettingsType(object): - """ - The SettingsType enum defines the possible values for the settings files - that are available for extension in the edx-platform. Plugin apps use these - values (in addition to ProjectType) to declare explicitly which settings - (in the specified project) they are extending. - - See https://github.com/edx/edx-platform/master/lms/envs/docs/README.rst for - further information on each Settings Type. - """ - AWS = u'aws' - COMMON = u'common' - DEVSTACK = u'devstack' - TEST = u'test' - - -class PluginSettings(object): - """ - The PluginSettings enum defines dictionary field names (and defaults) - that can be specified by a Plugin App in order to configure the settings - that are injected into the project. - """ - CONFIG = u'settings_config' - RELATIVE_PATH = u'relative_path' - DEFAULT_RELATIVE_PATH = u'settings' - - -class PluginURLs(object): - """ - The PluginURLs enum defines dictionary field names (and defaults) that can - be specified by a Plugin App in order to configure the URLs that are - injected into the project. - """ - CONFIG = u'url_config' - APP_NAME = u'app_name' - NAMESPACE = u'namespace' - REGEX = u'regex' - RELATIVE_PATH = u'relative_path' - DEFAULT_RELATIVE_PATH = u'urls' - - -class PluginSignals(object): - """ - The PluginSignals enum defines dictionary field names (and defaults) - that can be specified by a Plugin App in order to configure the signals - that it receives. - """ - CONFIG = u'signals_config' - - RECEIVERS = u'receivers' - DISPATCH_UID = u'dispatch_uid' - RECEIVER_FUNC_NAME = u'receiver_func_name' - SENDER_PATH = u'sender_path' - SIGNAL_PATH = u'signal_path' - - RELATIVE_PATH = u'relative_path' - DEFAULT_RELATIVE_PATH = u'signals' diff --git a/mocks/hawthorn/openedx/core/djangoapps/user_api/__init__.py b/mocks/hawthorn/openedx/core/djangoapps/user_api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/openedx/core/djangoapps/user_api/accounts/__init__.py b/mocks/hawthorn/openedx/core/djangoapps/user_api/accounts/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/openedx/core/djangoapps/user_api/accounts/serializers.py b/mocks/hawthorn/openedx/core/djangoapps/user_api/accounts/serializers.py deleted file mode 100644 index 745590e7..00000000 --- a/mocks/hawthorn/openedx/core/djangoapps/user_api/accounts/serializers.py +++ /dev/null @@ -1,47 +0,0 @@ - -from __future__ import absolute_import -from rest_framework import serializers - -# ReadOnlyFieldSerialzerMixin is from -# openedx.core.djangoapps.user_api.serializers - -class ReadOnlyFieldsSerializerMixin(object): - """ - Mixin for use with Serializers that provides a method - `get_read_only_fields`, which returns a tuple of all read-only - fields on the Serializer. - """ - @classmethod - def get_read_only_fields(cls): - """ - Return all fields on this Serializer class which are read-only. - Expects sub-classes implement Meta.explicit_read_only_fields, - which is a tuple declaring read-only fields which were declared - explicitly and thus could not be added to the usual - cls.Meta.read_only_fields tuple. - """ - return getattr(cls.Meta, 'read_only_fields', '') + getattr(cls.Meta, 'explicit_read_only_fields', '') - - @classmethod - def get_writeable_fields(cls): - """ - Return all fields on this serializer that are writeable. - """ - all_fields = getattr(cls.Meta, 'fields', tuple()) - return tuple(set(all_fields) - set(cls.get_read_only_fields())) - - -class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, ReadOnlyFieldsSerializerMixin): - - @staticmethod - def get_profile_image(user_profile, user, request=None): - ''' - For the mock, we probably can get by with just returning dummy data - ''' - return dict( - image_url_full= "http://localhost:8000/static/images/profiles/default_500.png", - image_url_large="http://localhost:8000/static/images/profiles/default_120.png", - image_url_medium="http://localhost:8000/static/images/profiles/default_50.png", - image_url_small="http://localhost:8000/static/images/profiles/default_30.png", - has_image=user_profile.has_profile_image, - ) diff --git a/mocks/hawthorn/openedx/core/djangoapps/xmodule_django/__init__.py b/mocks/hawthorn/openedx/core/djangoapps/xmodule_django/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/openedx/core/djangoapps/xmodule_django/models.py b/mocks/hawthorn/openedx/core/djangoapps/xmodule_django/models.py deleted file mode 100644 index ae0d642c..00000000 --- a/mocks/hawthorn/openedx/core/djangoapps/xmodule_django/models.py +++ /dev/null @@ -1,231 +0,0 @@ -"""Provides edx-platform functionality for Figures tests - -Provides Opaque key derived classes to support edx-platform mocks that use -CourseKeyField objects - -This is to make sure that course key related type errors are trapped in -figures tests:: - - "course-v1:MyOrg EdX101 2015_Spring" is not an instance of - - - -Code copied and modified as needed from: - -https://github.com/edx/edx-platform/blob/open-release/ginkgo.master/openedx/core/djangoapps/xmodule_django/models.py - -https://raw.githubusercontent.com/edx/opaque-keys/9807168660c12e0551c8fdd58fd1bc6b0bcb0a54/opaque_keys/edx/django/models.py - - -Useful django models for implementing XBlock infrastructure in django. -If Django is unavailable, none of the classes below will work as intended. -""" -from __future__ import absolute_import -import logging -import warnings - -try: - from django.core.exceptions import ValidationError - from django.db.models import CharField - from django.db.models.lookups import IsNull -except ImportError: # pragma: no cover - # Django is unavailable, none of the classes below will work, - # but we don't want the class definition to fail when interpreted. - CharField = object - IsNull = object - -import six - -from opaque_keys.edx.keys import BlockTypeKey, CourseKey, UsageKey - - -log = logging.getLogger(__name__) - - -class _Creator(object): - """ - DO NOT REUSE THIS CLASS. Provided for backwards compatibility only! - - A placeholder class that provides a way to set the attribute on the model. - """ - def __init__(self, field): - self.field = field - - def __get__(self, obj, type=None): # pylint: disable=redefined-builtin - if obj is None: - return self # pragma: no cover - return obj.__dict__[self.field.name] - - def __set__(self, obj, value): - obj.__dict__[self.field.name] = self.field.to_python(value) - - -# pylint: disable=missing-docstring,unused-argument -class CreatorMixin(object): - """ - Mixin class to provide SubfieldBase functionality to django fields. - See: https://docs.djangoproject.com/en/1.11/releases/1.8/#subfieldbase - """ - def contribute_to_class(self, cls, name, *args, **kwargs): - super(CreatorMixin, self).contribute_to_class(cls, name, *args, **kwargs) - setattr(cls, name, _Creator(self)) - - def from_db_value(self, value, expression, connection, context): - return self.to_python(value) - - -def _strip_object(key): - """ - Strips branch and version info if the given key supports those attributes. - """ - if hasattr(key, 'version_agnostic') and hasattr(key, 'for_branch'): - return key.for_branch(None).version_agnostic() - else: - return key - - -def _strip_value(value, lookup='exact'): - """ - Helper function to remove the branch and version information from the given value, - which could be a single object or a list. - """ - if lookup == 'in': - stripped_value = [_strip_object(el) for el in value] - else: - stripped_value = _strip_object(value) - return stripped_value - - -# pylint: disable=logging-format-interpolation -class OpaqueKeyField(CreatorMixin, CharField): - """ - A django field for storing OpaqueKeys. - - The baseclass will return the value from the database as a string, rather than an instance - of an OpaqueKey, leaving the application to determine which key subtype to parse the string - as. - - Subclasses must specify a KEY_CLASS attribute, in which case the field will use :meth:`from_string` - to parse the key string, and will return an instance of KEY_CLASS. - """ - description = "An OpaqueKey object, saved to the DB in the form of a string." - - Empty = object() - KEY_CLASS = None - - def __init__(self, *args, **kwargs): - if self.KEY_CLASS is None: - raise ValueError('Must specify KEY_CLASS in OpaqueKeyField subclasses') - - super(OpaqueKeyField, self).__init__(*args, **kwargs) - - def to_python(self, value): - if value is self.Empty or value is None: - return None - - error_message = "%s is not an instance of six.string_types or %s" % (value, self.KEY_CLASS) - assert isinstance(value, six.string_types + (self.KEY_CLASS,)), error_message - if value == '': - # handle empty string for models being created w/o fields populated - return None - - if isinstance(value, six.string_types): - if value.endswith('\n'): - # An opaque key with a trailing newline has leaked into the DB. - # Log and strip the value. - log.warning(u'{}:{}:{}:to_python: Invalid key: {}. Removing trailing newline.'.format( - self.model._meta.db_table, # pylint: disable=protected-access - self.name, - self.KEY_CLASS.__name__, - repr(value) - )) - value = value.rstrip() - return self.KEY_CLASS.from_string(value) - else: - return value - - def get_prep_value(self, value): - if value is self.Empty or value is None: - return '' # CharFields should use '' as their empty value, rather than None - - if isinstance(value, six.string_types): - value = self.KEY_CLASS.from_string(value) - - assert isinstance(value, self.KEY_CLASS), "%s is not an instance of %s" % (value, self.KEY_CLASS) - serialized_key = six.text_type(_strip_value(value)) - if serialized_key.endswith('\n'): - # An opaque key object serialized to a string with a trailing newline. - # Log the value - but do not modify it. - log.warning(u'{}:{}:{}:get_prep_value: Invalid key: {}.'.format( - self.model._meta.db_table, # pylint: disable=protected-access - self.name, - self.KEY_CLASS.__name__, - repr(serialized_key) - )) - return serialized_key - - def validate(self, value, model_instance): - """Validate Empty values, otherwise defer to the parent""" - # raise validation error if the use of this field says it can't be blank but it is - if not self.blank and value is self.Empty: - raise ValidationError(self.error_messages['blank']) - else: - return super(OpaqueKeyField, self).validate(value, model_instance) - - def run_validators(self, value): - """Validate Empty values, otherwise defer to the parent""" - if value is self.Empty: - return - - return super(OpaqueKeyField, self).run_validators(value) - - -class OpaqueKeyFieldEmptyLookupIsNull(IsNull): - """ - This overrides the default __isnull model filter to help enforce the special way - we handle null / empty values in OpaqueKeyFields. - """ - def get_prep_lookup(self): - raise TypeError("Use this field's .Empty member rather than None or __isnull " - "to query for missing objects of this type.") - - -try: - # pylint: disable=no-member - OpaqueKeyField.register_lookup(OpaqueKeyFieldEmptyLookupIsNull) -except AttributeError: - # Django was not imported - pass - - -class CourseKeyField(OpaqueKeyField): - """ - A django Field that stores a CourseKey object as a string. - """ - description = "A CourseKey object, saved to the DB in the form of a string" - KEY_CLASS = CourseKey - - -class UsageKeyField(OpaqueKeyField): - """ - A django Field that stores a UsageKey object as a string. - """ - description = "A Location object, saved to the DB in the form of a string" - KEY_CLASS = UsageKey - - -class LocationKeyField(UsageKeyField): - """ - A django Field that stores a UsageKey object as a string. - """ - def __init__(self, *args, **kwargs): - warnings.warn("LocationKeyField is deprecated. Please use UsageKeyField instead.", stacklevel=2) - super(LocationKeyField, self).__init__(*args, **kwargs) - - -class BlockTypeKeyField(OpaqueKeyField): - """ - A django Field that stores a BlockTypeKey object as a string. - """ - description = "A BlockTypeKey object, saved to the DB in the form of a string." - KEY_CLASS = BlockTypeKey diff --git a/mocks/hawthorn/openedx/core/release.py b/mocks/hawthorn/openedx/core/release.py deleted file mode 100644 index 698f853a..00000000 --- a/mocks/hawthorn/openedx/core/release.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -Identify the release of Open edX platform -""" - -RELEASE_LINE = 'hawthorn' - diff --git a/mocks/hawthorn/student/__init__.py b/mocks/hawthorn/student/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/student/migrations/0001_initial.py b/mocks/hawthorn/student/migrations/0001_initial.py deleted file mode 100644 index 9b612910..00000000 --- a/mocks/hawthorn/student/migrations/0001_initial.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.15 on 2019-11-29 21:26 -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import django_countries.fields -import openedx.core.djangoapps.xmodule_django.models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('course_overviews', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='CourseAccessRole', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('org', models.CharField(blank=True, db_index=True, max_length=64)), - ('course_id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(blank=True, db_index=True, max_length=255)), - ('role', models.CharField(db_index=True, max_length=64)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='CourseEnrollment', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(null=True)), - ('is_active', models.BooleanField(default=True)), - ('mode', models.CharField(default=b'audit', max_length=100)), - ('course', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, to='course_overviews.CourseOverview')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ('user', 'course'), - }, - ), - migrations.CreateModel( - name='UserProfile', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(blank=True, db_index=True, max_length=255)), - ('meta', models.TextField(blank=True)), - ('courseware', models.CharField(blank=True, default=b'course.xml', max_length=255)), - ('language', models.CharField(blank=True, db_index=True, max_length=255)), - ('location', models.CharField(blank=True, db_index=True, max_length=255)), - ('year_of_birth', models.IntegerField(blank=True, db_index=True, null=True)), - ('gender', models.CharField(blank=True, choices=[(b'm', b'Male'), (b'f', b'Female'), (b'o', b'Other/Prefer Not to Say')], db_index=True, max_length=6, null=True)), - ('level_of_education', models.CharField(blank=True, choices=[(b'p', b'Doctorate'), (b'm', b"Master's or professional degree"), (b'b', b"Bachelor's degree"), (b'a', b'Associate degree'), (b'hs', b'Secondary/high school'), (b'jhs', b'Junior secondary/junior high/middle school'), (b'el', b'Elementary/primary school'), (b'none', b'No formal education'), (b'other', b'Other education')], db_index=True, max_length=6, null=True)), - ('mailing_address', models.TextField(blank=True, null=True)), - ('city', models.TextField(blank=True, null=True)), - ('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)), - ('goals', models.TextField(blank=True, null=True)), - ('allow_certificate', models.BooleanField(default=1)), - ('bio', models.CharField(blank=True, max_length=3000, null=True)), - ('profile_image_uploaded_at', models.DateTimeField(blank=True, null=True)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.AlterUniqueTogether( - name='courseenrollment', - unique_together=set([('user', 'course')]), - ), - migrations.AlterUniqueTogether( - name='courseaccessrole', - unique_together=set([('user', 'org', 'course_id', 'role')]), - ), - ] diff --git a/mocks/hawthorn/student/migrations/__init__.py b/mocks/hawthorn/student/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/student/models.py b/mocks/hawthorn/student/models.py deleted file mode 100644 index 9d0e0926..00000000 --- a/mocks/hawthorn/student/models.py +++ /dev/null @@ -1,274 +0,0 @@ -from collections import defaultdict, namedtuple -from datetime import datetime - -from pytz import UTC - -from django.db import models -from django.contrib.auth.models import User -from django.utils.translation import ugettext_noop - -from django_countries.fields import CountryField - -from course_modes.models import CourseMode - -from openedx.core.djangoapps.content.course_overviews.models import ( - CourseOverview, -) -from openedx.core.djangoapps.xmodule_django.models import CourseKeyField -import six -from six.moves import range - -class UserProfile(models.Model): - ''' - The production model is student.models.UserProfile - ''' - user = models.OneToOneField(User, unique=True, db_index=True, - related_name='profile', on_delete=models.CASCADE) - name = models.CharField(blank=True, max_length=255, db_index=True) - meta = models.TextField(blank=True) # JSON dictionary for future expansion - courseware = models.CharField(blank=True, max_length=255, default='course.xml') - - # Location is no longer used, but is held here for backwards compatibility - # for users imported from our first class. - language = models.CharField(blank=True, max_length=255, db_index=True) - location = models.CharField(blank=True, max_length=255, db_index=True) - - # Optional demographic data we started capturing from Fall 2012 - this_year = datetime.now(UTC).year - VALID_YEARS = list(range(this_year, this_year - 120, -1)) - year_of_birth = models.IntegerField(blank=True, null=True, db_index=True) - - GENDER_CHOICES = ( - ('m', ugettext_noop('Male')), - ('f', ugettext_noop('Female')), - # Translators: 'Other' refers to the student's gender - ('o', ugettext_noop('Other/Prefer Not to Say')) - ) - gender = models.CharField( - blank=True, null=True, max_length=6, db_index=True, choices=GENDER_CHOICES - ) - - # [03/21/2013] removed these, but leaving comment since there'll still be - # p_se and p_oth in the existing data in db. - # ('p_se', 'Doctorate in science or engineering'), - # ('p_oth', 'Doctorate in another field'), - LEVEL_OF_EDUCATION_CHOICES = ( - ('p', ugettext_noop('Doctorate')), - ('m', ugettext_noop("Master's or professional degree")), - ('b', ugettext_noop("Bachelor's degree")), - ('a', ugettext_noop("Associate degree")), - ('hs', ugettext_noop("Secondary/high school")), - ('jhs', ugettext_noop("Junior secondary/junior high/middle school")), - ('el', ugettext_noop("Elementary/primary school")), - # Translators: 'None' refers to the student's level of education - ('none', ugettext_noop("No formal education")), - # Translators: 'Other' refers to the student's level of education - ('other', ugettext_noop("Other education")) - ) - level_of_education = models.CharField( - blank=True, null=True, max_length=6, db_index=True, - choices=LEVEL_OF_EDUCATION_CHOICES - ) - mailing_address = models.TextField(blank=True, null=True) - city = models.TextField(blank=True, null=True) - country = CountryField(blank=True, null=True) - goals = models.TextField(blank=True, null=True) - allow_certificate = models.BooleanField(default=1) - bio = models.CharField(blank=True, null=True, max_length=3000, db_index=False) - profile_image_uploaded_at = models.DateTimeField(null=True, blank=True) - - @property - def has_profile_image(self): - """ - Convenience method that returns a boolean indicating whether or not - this user has uploaded a profile image. - """ - return self.profile_image_uploaded_at is not None - - -class CourseEnrollmentManager(models.Manager): - - def num_enrolled_in_exclude_admins(self, course_id): - """ - Returns the count of active enrollments in a course excluding instructors, staff and CCX coaches. - - Arguments: - course_id (CourseLocator): course_id to return enrollments (count). - - Returns: - int: Count of enrollments excluding staff, instructors and CCX coaches. - - """ - # To avoid circular imports. - from student.roles import CourseCcxCoachRole, CourseInstructorRole, CourseStaffRole - course_locator = course_id - - if getattr(course_id, 'ccx', None): - # We don't use CCX, so raising exception rather than support it - raise Exception('CCX is not supported') - - staff = CourseStaffRole(course_locator).users_with_role() - admins = CourseInstructorRole(course_locator).users_with_role() - coaches = CourseCcxCoachRole(course_locator).users_with_role() - - qs = super(CourseEnrollmentManager, self).get_queryset() - q2 = qs.filter(course_id=course_id, is_active=1) - q3 = q2.exclude(user__in=staff).exclude(user__in=admins).exclude(user__in=coaches) - return q3.count() - - def enrollment_counts(self, course_id): - - query = super(CourseEnrollmentManager, self).get_queryset().filter( - course_id=course_id, is_active=True).values( - 'mode').order_by().annotate(models.Count('mode')) - total = 0 - enroll_dict = defaultdict(int) - for item in query: - enroll_dict[item['mode']] = item['mode__count'] - total += item['mode__count'] - enroll_dict['total'] = total - return enroll_dict - - -# Named tuple for fields pertaining to the state of -# CourseEnrollment for a user in a course. This type -# is used to cache the state in the request cache. -CourseEnrollmentState = namedtuple('CourseEnrollmentState', 'mode, is_active') - - -class CourseEnrollment(models.Model): - ''' - The production model is student.models.CourseEnrollment - - The purpose of this mock is to provide the model needed to - retrieve: - * The learners enrolled in a course - * When a learner enrolled - * If the learner is active - ''' - - MODEL_TAGS = ['course', 'is_active', 'mode'] - - user = models.ForeignKey(User, on_delete=models.CASCADE) - - course = models.ForeignKey( - CourseOverview, - db_constraint=False, - on_delete=models.DO_NOTHING, - ) - - @property - def course_id(self): - return self._course_id - - @course_id.setter - def course_id(self, value): - if isinstance(value, six.string_types): - self._course_id = CourseKey.from_string(value) - else: - self._course_id = value - - # NOTE: We do not want to enable `auto_now_add` because we need the factory - # to set the created date - created = models.DateTimeField(null=True) - - # If is_active is False, then the student is not considered to be enrolled - # in the course (is_enrolled() will return False) - is_active = models.BooleanField(default=True) - - # Represents the modes that are possible. We'll update this later with a - # list of possible values. - mode = models.CharField(default=CourseMode.DEFAULT_MODE_SLUG, max_length=100) - - objects = CourseEnrollmentManager() - - # # cache key format e.g enrollment...mode = 'honor' - # COURSE_ENROLLMENT_CACHE_KEY = u"enrollment.{}.{}.mode" # TODO Can this be removed? It doesn't seem to be used. - - # MODE_CACHE_NAMESPACE = u'CourseEnrollment.mode_and_active' - - class Meta(object): - unique_together = (('user', 'course'),) - ordering = ('user', 'course') - - def __init__(self, *args, **kwargs): - super(CourseEnrollment, self).__init__(*args, **kwargs) - - # Private variable for storing course_overview to minimize calls to the database. - # When the property .course_overview is accessed for the first time, this variable will be set. - self._course_overview = None - - @classmethod - def is_enrolled(cls, user, course_key): - """ - Returns True if the user is enrolled in the course (the entry must exist - and it must have `is_active=True`). Otherwise, returns False. - - `user` is a Django User object. If it hasn't been saved yet (no `.id` - attribute), this method will automatically save it before - adding an enrollment for it. - - `course_id` is our usual course_id string (e.g. "edX/Test101/2013_Fall) - """ - enrollment_state = cls._get_enrollment_state(user, course_key) - return enrollment_state.is_active or False - - @classmethod - def _get_enrollment_state(cls, user, course_key): - """ - Returns the CourseEnrollmentState for the given user - and course_key, caching the result for later retrieval. - - Figures note: removed the caching after copying this method - """ - assert user - - if user.is_anonymous: - return CourseEnrollmentState(None, None) - - try: - record = cls.objects.get(user=user, course_id=course_key) - enrollment_state = CourseEnrollmentState(record.mode, record.is_active) - except cls.DoesNotExist: - enrollment_state = CourseEnrollmentState(None, None) - - return enrollment_state - - -class CourseAccessRole(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE,) - # blank org is for global group based roles such as course creator (may be deprecated) - org = models.CharField(max_length=64, db_index=True, blank=True) - # blank course_id implies org wide role - course_id = CourseKeyField(max_length=255, db_index=True, blank=True) - role = models.CharField(max_length=64, db_index=True) - - class Meta(object): - unique_together = ('user', 'org', 'course_id', 'role') - - @property - def _key(self): - """ - convenience function to make eq overrides easier and clearer. arbitrary decision - that role is primary, followed by org, course, and then user - """ - return (self.role, self.org, self.course_id, self.user_id) - - def __eq__(self, other): - """ - Overriding eq b/c the django impl relies on the primary key which requires fetch. sometimes we - just want to compare roles w/o doing another fetch. - """ - return type(self) == type(other) and self._key == other._key # pylint: disable=protected-access - - def __hash__(self): - return hash(self._key) - - def __lt__(self, other): - """ - Lexigraphic sort - """ - return self._key < other._key # pylint: disable=protected-access - - def __unicode__(self): - return "[CourseAccessRole] user: {} role: {} org: {} course: {}".format(self.user.username, self.role, self.org, self.course_id) diff --git a/mocks/hawthorn/student/roles.py b/mocks/hawthorn/student/roles.py deleted file mode 100644 index e2f776b9..00000000 --- a/mocks/hawthorn/student/roles.py +++ /dev/null @@ -1,60 +0,0 @@ -''' -Mocks role classes needed in Figures tests -''' - -from __future__ import absolute_import -from django.contrib.auth.models import User - -from openedx.core.djangoapps.xmodule_django.models import CourseKeyField -from student.models import CourseAccessRole - -class MockCourseRole(object): - ''' - Mock student.models.CourseRole and its parent classes - - Guideline: only implement the minimum needed to simulate edx-platform for - the Figures unit tests - ''' - def __init__(self, role, course_key): - - # The following are declared in studen.roles.RoleBase - self.org = '' - self._role_name = role - # The following are declared in student.roles.CourseRole - self.role = role - self.course_key = course_key - - def users_with_role(self): - """ - Return a django QuerySet for all of the users with this role - """ - # Org roles don't query by CourseKey, so use CourseKeyField.Empty for that query - if self.course_key is None: - self.course_key = CourseKeyField.Empty - entries = User.objects.filter( - courseaccessrole__role=self._role_name, - courseaccessrole__org=self.org, - courseaccessrole__course_id=self.course_key - ) - return entries - - -class CourseCcxCoachRole(MockCourseRole): - ROLE = 'ccx_coach' - - def __init__(self, *args, **kwargs): - super(CourseCcxCoachRole, self).__init__(self.ROLE, *args, **kwargs) - - -class CourseInstructorRole(MockCourseRole): - ROLE = 'instructor' - - def __init__(self, *args, **kwargs): - super(CourseInstructorRole, self).__init__(self.ROLE, *args, **kwargs) - - -class CourseStaffRole(MockCourseRole): - ROLE = 'staff' - - def __init__(self, *args, **kwargs): - super(CourseStaffRole, self).__init__(self.ROLE, *args, **kwargs) diff --git a/mocks/hawthorn/xmodule/__init__.py b/mocks/hawthorn/xmodule/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/xmodule/modulestore/__init__.py b/mocks/hawthorn/xmodule/modulestore/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/hawthorn/xmodule/modulestore/django.py b/mocks/hawthorn/xmodule/modulestore/django.py deleted file mode 100644 index 5ee7b6bb..00000000 --- a/mocks/hawthorn/xmodule/modulestore/django.py +++ /dev/null @@ -1,72 +0,0 @@ -''' - -./common/lib/xmodule/xmodule/modulestore/django.py -''' - -from __future__ import absolute_import -from contextlib import contextmanager - - -class MockCourse(object): - ''' - This is a mock of the CourseDescriptor - - This usually seems to be a 'xblock.internal.CourseDescriptorWithMixins' - object. Which appears to be an ephemeral class (yeah, let that sink in). - which makes working with it a real joy since it also appears to not be - documented anywhere, even though it might be one of the most important - classes in Open edX. - - There is a 'CourseDescriptor' class in - ``common/lib/xmodule/xmodule/course_module.py`` - - So this mock is a minimal attempt - ''' - note = 'I am a mock course object.' - - def __init__(self, course_locator, **kwargs): - self.grading_policy = None - self.id = course_locator - def set_grading_policy(self, grading_policy): - self.grading_policy = grading_policy - - -class MockMixedModulestore(object): - ''' - We are mocking functionalit needed by courseware.courses - ''' - def __init__(self, **kwargs): - pass - - @contextmanager - def bulk_operations(self, course_id, emit_signals=True, ignore_Case=False): - ''' - see xmodule/modulestore/__init__.py - - Bulk operations look like a likely candidate to extract from edx-platform - - For now, we're doing the absolute minimum mocking - ''' - yield - - def get_course(self, course_id, depth): - return MockCourse(course_locator=course_id) - -def modulestore(): - ''' - Need to mock: - - with modulestore().bulk_operations(course_key): - course = modulestore().get_course(course_key, depth=depth) - ''' - - # This is the production code: - # We are skipping mocking the CCX modulestore - # _MIXED_MODULESTORE = create_modulestore_instance( - # settings.MODULESTORE['default']['ENGINE'], - # contentstore(), - # settings.MODULESTORE['default'].get('DOC_STORE_CONFIG', {}), - # settings.MODULESTORE['default'].get('OPTIONS', {}) - # ) - - return MockMixedModulestore() diff --git a/mocks/juniper/__init__.py b/mocks/juniper/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/course_modes/__init__.py b/mocks/juniper/course_modes/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/lms/__init__.py b/mocks/juniper/lms/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/lms/djangoapps/__init__.py b/mocks/juniper/lms/djangoapps/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/lms/djangoapps/certificates/__init__.py b/mocks/juniper/lms/djangoapps/certificates/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/lms/djangoapps/certificates/migrations/__init__.py b/mocks/juniper/lms/djangoapps/certificates/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/lms/djangoapps/courseware/__init__.py b/mocks/juniper/lms/djangoapps/courseware/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/lms/djangoapps/courseware/migrations/__init__.py b/mocks/juniper/lms/djangoapps/courseware/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/lms/djangoapps/grades/__init__.py b/mocks/juniper/lms/djangoapps/grades/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/lms/djangoapps/grades/course_grade.py b/mocks/juniper/lms/djangoapps/grades/course_grade.py deleted file mode 100644 index ff55203c..00000000 --- a/mocks/juniper/lms/djangoapps/grades/course_grade.py +++ /dev/null @@ -1,108 +0,0 @@ -''' - -''' -from __future__ import absolute_import -from collections import OrderedDict - - -class MockAggregatedScore(object): - ''' - - ''' - def __init__(self, tw_earned, tw_possible, **kwargs): - self.graded = False - self.first_attempted = None - self.earned = float(tw_earned) if tw_earned is not None else None - self.possible = float(tw_possible) if tw_possible is not None else None - - def __str__(self): - return 'earned={}, possible={}, graded={}, first_attempted={}'.format( - self.earned, self.possible, self.graded, self.first_attempted) - - def __repr__(self): - return self.__str__() - - -class MockSubsectionGrade(object): - ''' - - ''' - def __init__(self, **kwargs): - self.problem_scores = OrderedDict() - self.all_total = MockAggregatedScore( - tw_earned=kwargs.get('tw_earned', 0.0), - tw_possible=kwargs.get('tw_possible', 0.0) - ) - - def __str__(self): - return 'all_total={}'.format(self.all_total) - - def __repr__(self): - return self.__str__() - -def create_chapter_grades(): - ''' - Mock for course_grades.CourseGradeBase.chapter_grades - (which uses the @lazy decorator) - - for chapter_grade in self.course_grade.chapter_grades.values(): - for section in chapter_grade['sections']: - - we don't need the BlockUsageLocator key for our initial testing - So we're just going to use the phonetic alphabet - - Use the following to generate additional url names - ``binascii.b2a_hex(os.urandom(16))`` - - Chapter grade keys are 'sections', 'url_name', and 'display_name' - ''' - - return OrderedDict( - alpha=dict( - sections=[ - MockSubsectionGrade(tw_earned=0.0, tw_possible=0.0), - MockSubsectionGrade(tw_earned=0.0,tw_possible=0.5), - MockSubsectionGrade(tw_earned=0.5,tw_possible=1.0), - ], - url_name=u'ec7e84694fca2d073731a462a5916a7a', - display_name=u'Module 1 - Overview', - ), - bravo=dict( - sections=[ - MockSubsectionGrade(), - MockSubsectionGrade(), - MockSubsectionGrade(), - ], - url_name=u'2f43c0b7da59ed40156155f9a8ca4d40', - display_name=u'Module 2 - First Principles', - ), - ) - - -class CourseGrade(object): - ''' - Production class inherits from CourseGradeBase - ''' - def __init__(self, user, course_data, percent=0, letter_grade=None, passed=False, *args, **kwargs): - self.user = user - self.course_data = course_data, - self.percent = percent - self.passed = passed - - # Convert empty strings to None when reading from the table - self.letter_grade = letter_grade or None - self.chapter_grades = kwargs.get('chapter_grades', - create_chapter_grades()) - - @property - def summary(self): - """ - Returns the grade summary as calculated by the course's grader. - DEPRECATED: To be removed as part of TNL-5291. - """ - # TODO(TNL-5291) Remove usages of this deprecated property. - # grade_summary = self.grader_result - grade_summary = {} - grade_summary['percent'] = self.percent - grade_summary['grade'] = self.letter_grade - return grade_summary diff --git a/mocks/juniper/lms/djangoapps/teams/__init__.py b/mocks/juniper/lms/djangoapps/teams/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/openedx/__init__.py b/mocks/juniper/openedx/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/openedx/core/__init__.py b/mocks/juniper/openedx/core/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/openedx/core/djangoapps/__init__.py b/mocks/juniper/openedx/core/djangoapps/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/openedx/core/djangoapps/content/__init__.py b/mocks/juniper/openedx/core/djangoapps/content/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/openedx/core/djangoapps/content/course_overviews/__init__.py b/mocks/juniper/openedx/core/djangoapps/content/course_overviews/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/openedx/core/djangoapps/content/course_overviews/migrations/__init__.py b/mocks/juniper/openedx/core/djangoapps/content/course_overviews/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/openedx/core/djangoapps/course_groups/__init__.py b/mocks/juniper/openedx/core/djangoapps/course_groups/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/openedx/core/djangoapps/course_groups/migrations/0001_initial.py b/mocks/juniper/openedx/core/djangoapps/course_groups/migrations/0001_initial.py deleted file mode 100644 index 08958011..00000000 --- a/mocks/juniper/openedx/core/djangoapps/course_groups/migrations/0001_initial.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.15 on 2019-08-13 20:33 -from __future__ import unicode_literals - -from __future__ import absolute_import -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import opaque_keys.edx.django.models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='CohortMembership', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('course_id', opaque_keys.edx.django.models.CourseKeyField(max_length=255)), - ], - ), - migrations.CreateModel( - name='CourseUserGroup', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(help_text=b'What is the name of this group? Must be unique within a course.', max_length=255)), - ('course_id', opaque_keys.edx.django.models.CourseKeyField(db_index=True, help_text=b'Which course is this group associated with?', max_length=255)), - ('group_type', models.CharField(choices=[(b'cohort', b'Cohort')], max_length=20)), - ('users', models.ManyToManyField(db_index=True, help_text=b'Who is in this group?', related_name='course_groups', to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.AddField( - model_name='cohortmembership', - name='course_user_group', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course_groups.CourseUserGroup'), - ), - migrations.AddField( - model_name='cohortmembership', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - migrations.AlterUniqueTogether( - name='courseusergroup', - unique_together=set([('name', 'course_id')]), - ), - migrations.AlterUniqueTogether( - name='cohortmembership', - unique_together=set([('user', 'course_id')]), - ), - ] diff --git a/mocks/juniper/openedx/core/djangoapps/course_groups/migrations/__init__.py b/mocks/juniper/openedx/core/djangoapps/course_groups/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/openedx/core/djangoapps/plugins/__init__.py b/mocks/juniper/openedx/core/djangoapps/plugins/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/openedx/core/djangoapps/user_api/__init__.py b/mocks/juniper/openedx/core/djangoapps/user_api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/openedx/core/djangoapps/user_api/accounts/__init__.py b/mocks/juniper/openedx/core/djangoapps/user_api/accounts/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/openedx/core/djangoapps/user_api/accounts/serializers.py b/mocks/juniper/openedx/core/djangoapps/user_api/accounts/serializers.py deleted file mode 100644 index 745590e7..00000000 --- a/mocks/juniper/openedx/core/djangoapps/user_api/accounts/serializers.py +++ /dev/null @@ -1,47 +0,0 @@ - -from __future__ import absolute_import -from rest_framework import serializers - -# ReadOnlyFieldSerialzerMixin is from -# openedx.core.djangoapps.user_api.serializers - -class ReadOnlyFieldsSerializerMixin(object): - """ - Mixin for use with Serializers that provides a method - `get_read_only_fields`, which returns a tuple of all read-only - fields on the Serializer. - """ - @classmethod - def get_read_only_fields(cls): - """ - Return all fields on this Serializer class which are read-only. - Expects sub-classes implement Meta.explicit_read_only_fields, - which is a tuple declaring read-only fields which were declared - explicitly and thus could not be added to the usual - cls.Meta.read_only_fields tuple. - """ - return getattr(cls.Meta, 'read_only_fields', '') + getattr(cls.Meta, 'explicit_read_only_fields', '') - - @classmethod - def get_writeable_fields(cls): - """ - Return all fields on this serializer that are writeable. - """ - all_fields = getattr(cls.Meta, 'fields', tuple()) - return tuple(set(all_fields) - set(cls.get_read_only_fields())) - - -class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer, ReadOnlyFieldsSerializerMixin): - - @staticmethod - def get_profile_image(user_profile, user, request=None): - ''' - For the mock, we probably can get by with just returning dummy data - ''' - return dict( - image_url_full= "http://localhost:8000/static/images/profiles/default_500.png", - image_url_large="http://localhost:8000/static/images/profiles/default_120.png", - image_url_medium="http://localhost:8000/static/images/profiles/default_50.png", - image_url_small="http://localhost:8000/static/images/profiles/default_30.png", - has_image=user_profile.has_profile_image, - ) diff --git a/mocks/juniper/openedx/core/djangoapps/xmodule_django/__init__.py b/mocks/juniper/openedx/core/djangoapps/xmodule_django/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/openedx/core/release.py b/mocks/juniper/openedx/core/release.py deleted file mode 100644 index 8bfc3a4f..00000000 --- a/mocks/juniper/openedx/core/release.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Identify the release of Open edX platform -""" - -RELEASE_LINE = 'juniper' diff --git a/mocks/juniper/student/__init__.py b/mocks/juniper/student/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/student/migrations/__init__.py b/mocks/juniper/student/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/xmodule/__init__.py b/mocks/juniper/xmodule/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/xmodule/modulestore/__init__.py b/mocks/juniper/xmodule/modulestore/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mocks/juniper/xmodule/modulestore/exceptions.py b/mocks/juniper/xmodule/modulestore/exceptions.py deleted file mode 100644 index 3920e97f..00000000 --- a/mocks/juniper/xmodule/modulestore/exceptions.py +++ /dev/null @@ -1,2 +0,0 @@ -class ItemNotFoundError(Exception): - pass diff --git a/mocks/ginkgo/lms/__init__.py b/mocks/lms/__init__.py similarity index 100% rename from mocks/ginkgo/lms/__init__.py rename to mocks/lms/__init__.py diff --git a/mocks/ginkgo/lms/djangoapps/__init__.py b/mocks/lms/djangoapps/__init__.py similarity index 100% rename from mocks/ginkgo/lms/djangoapps/__init__.py rename to mocks/lms/djangoapps/__init__.py diff --git a/mocks/ginkgo/lms/djangoapps/grades/__init__.py b/mocks/lms/djangoapps/certificates/__init__.py similarity index 100% rename from mocks/ginkgo/lms/djangoapps/grades/__init__.py rename to mocks/lms/djangoapps/certificates/__init__.py diff --git a/mocks/juniper/lms/djangoapps/certificates/migrations/0001_initial.py b/mocks/lms/djangoapps/certificates/migrations/0001_initial.py similarity index 100% rename from mocks/juniper/lms/djangoapps/certificates/migrations/0001_initial.py rename to mocks/lms/djangoapps/certificates/migrations/0001_initial.py diff --git a/mocks/ginkgo/lms/djangoapps/grades/new/__init__.py b/mocks/lms/djangoapps/certificates/migrations/__init__.py similarity index 100% rename from mocks/ginkgo/lms/djangoapps/grades/new/__init__.py rename to mocks/lms/djangoapps/certificates/migrations/__init__.py diff --git a/mocks/juniper/lms/djangoapps/certificates/models.py b/mocks/lms/djangoapps/certificates/models.py similarity index 100% rename from mocks/juniper/lms/djangoapps/certificates/models.py rename to mocks/lms/djangoapps/certificates/models.py diff --git a/mocks/ginkgo/lms/djangoapps/teams/__init__.py b/mocks/lms/djangoapps/courseware/__init__.py similarity index 100% rename from mocks/ginkgo/lms/djangoapps/teams/__init__.py rename to mocks/lms/djangoapps/courseware/__init__.py diff --git a/mocks/juniper/lms/djangoapps/courseware/courses.py b/mocks/lms/djangoapps/courseware/courses.py similarity index 100% rename from mocks/juniper/lms/djangoapps/courseware/courses.py rename to mocks/lms/djangoapps/courseware/courses.py diff --git a/mocks/juniper/lms/djangoapps/courseware/migrations/0001_initial.py b/mocks/lms/djangoapps/courseware/migrations/0001_initial.py similarity index 100% rename from mocks/juniper/lms/djangoapps/courseware/migrations/0001_initial.py rename to mocks/lms/djangoapps/courseware/migrations/0001_initial.py diff --git a/mocks/ginkgo/openedx/__init__.py b/mocks/lms/djangoapps/courseware/migrations/__init__.py similarity index 100% rename from mocks/ginkgo/openedx/__init__.py rename to mocks/lms/djangoapps/courseware/migrations/__init__.py diff --git a/mocks/juniper/lms/djangoapps/courseware/models.py b/mocks/lms/djangoapps/courseware/models.py similarity index 100% rename from mocks/juniper/lms/djangoapps/courseware/models.py rename to mocks/lms/djangoapps/courseware/models.py diff --git a/mocks/ginkgo/openedx/core/__init__.py b/mocks/lms/djangoapps/grades/__init__.py similarity index 100% rename from mocks/ginkgo/openedx/core/__init__.py rename to mocks/lms/djangoapps/grades/__init__.py diff --git a/mocks/hawthorn/lms/djangoapps/grades/course_grade.py b/mocks/lms/djangoapps/grades/course_grade.py similarity index 100% rename from mocks/hawthorn/lms/djangoapps/grades/course_grade.py rename to mocks/lms/djangoapps/grades/course_grade.py diff --git a/mocks/juniper/lms/djangoapps/grades/course_grade_factory.py b/mocks/lms/djangoapps/grades/course_grade_factory.py similarity index 100% rename from mocks/juniper/lms/djangoapps/grades/course_grade_factory.py rename to mocks/lms/djangoapps/grades/course_grade_factory.py diff --git a/mocks/ginkgo/openedx/core/djangoapps/__init__.py b/mocks/lms/djangoapps/teams/__init__.py similarity index 100% rename from mocks/ginkgo/openedx/core/djangoapps/__init__.py rename to mocks/lms/djangoapps/teams/__init__.py diff --git a/mocks/juniper/lms/djangoapps/teams/models.py b/mocks/lms/djangoapps/teams/models.py similarity index 100% rename from mocks/juniper/lms/djangoapps/teams/models.py rename to mocks/lms/djangoapps/teams/models.py diff --git a/mocks/ginkgo/openedx/core/djangoapps/content/__init__.py b/mocks/openedx/__init__.py similarity index 100% rename from mocks/ginkgo/openedx/core/djangoapps/content/__init__.py rename to mocks/openedx/__init__.py diff --git a/mocks/ginkgo/openedx/core/djangoapps/content/course_overviews/__init__.py b/mocks/openedx/core/__init__.py similarity index 100% rename from mocks/ginkgo/openedx/core/djangoapps/content/course_overviews/__init__.py rename to mocks/openedx/core/__init__.py diff --git a/mocks/ginkgo/openedx/core/djangoapps/course_groups/__init__.py b/mocks/openedx/core/djangoapps/__init__.py similarity index 100% rename from mocks/ginkgo/openedx/core/djangoapps/course_groups/__init__.py rename to mocks/openedx/core/djangoapps/__init__.py diff --git a/mocks/ginkgo/openedx/core/djangoapps/course_groups/migrations/__init__.py b/mocks/openedx/core/djangoapps/content/__init__.py similarity index 100% rename from mocks/ginkgo/openedx/core/djangoapps/course_groups/migrations/__init__.py rename to mocks/openedx/core/djangoapps/content/__init__.py diff --git a/mocks/ginkgo/openedx/core/djangoapps/user_api/__init__.py b/mocks/openedx/core/djangoapps/content/course_overviews/__init__.py similarity index 100% rename from mocks/ginkgo/openedx/core/djangoapps/user_api/__init__.py rename to mocks/openedx/core/djangoapps/content/course_overviews/__init__.py diff --git a/mocks/juniper/openedx/core/djangoapps/content/course_overviews/migrations/0001_initial.py b/mocks/openedx/core/djangoapps/content/course_overviews/migrations/0001_initial.py similarity index 100% rename from mocks/juniper/openedx/core/djangoapps/content/course_overviews/migrations/0001_initial.py rename to mocks/openedx/core/djangoapps/content/course_overviews/migrations/0001_initial.py diff --git a/mocks/ginkgo/openedx/core/djangoapps/user_api/accounts/__init__.py b/mocks/openedx/core/djangoapps/content/course_overviews/migrations/__init__.py similarity index 100% rename from mocks/ginkgo/openedx/core/djangoapps/user_api/accounts/__init__.py rename to mocks/openedx/core/djangoapps/content/course_overviews/migrations/__init__.py diff --git a/mocks/juniper/openedx/core/djangoapps/content/course_overviews/models.py b/mocks/openedx/core/djangoapps/content/course_overviews/models.py similarity index 100% rename from mocks/juniper/openedx/core/djangoapps/content/course_overviews/models.py rename to mocks/openedx/core/djangoapps/content/course_overviews/models.py diff --git a/mocks/ginkgo/openedx/core/djangoapps/xmodule_django/__init__.py b/mocks/openedx/core/djangoapps/course_groups/__init__.py similarity index 100% rename from mocks/ginkgo/openedx/core/djangoapps/xmodule_django/__init__.py rename to mocks/openedx/core/djangoapps/course_groups/__init__.py diff --git a/mocks/hawthorn/openedx/core/djangoapps/course_groups/migrations/0001_initial.py b/mocks/openedx/core/djangoapps/course_groups/migrations/0001_initial.py similarity index 100% rename from mocks/hawthorn/openedx/core/djangoapps/course_groups/migrations/0001_initial.py rename to mocks/openedx/core/djangoapps/course_groups/migrations/0001_initial.py diff --git a/mocks/ginkgo/student/__init__.py b/mocks/openedx/core/djangoapps/course_groups/migrations/__init__.py similarity index 100% rename from mocks/ginkgo/student/__init__.py rename to mocks/openedx/core/djangoapps/course_groups/migrations/__init__.py diff --git a/mocks/juniper/openedx/core/djangoapps/course_groups/models.py b/mocks/openedx/core/djangoapps/course_groups/models.py similarity index 100% rename from mocks/juniper/openedx/core/djangoapps/course_groups/models.py rename to mocks/openedx/core/djangoapps/course_groups/models.py diff --git a/mocks/ginkgo/student/migrations/__init__.py b/mocks/openedx/core/djangoapps/plugins/__init__.py similarity index 100% rename from mocks/ginkgo/student/migrations/__init__.py rename to mocks/openedx/core/djangoapps/plugins/__init__.py diff --git a/mocks/juniper/openedx/core/djangoapps/plugins/constants.py b/mocks/openedx/core/djangoapps/plugins/constants.py similarity index 100% rename from mocks/juniper/openedx/core/djangoapps/plugins/constants.py rename to mocks/openedx/core/djangoapps/plugins/constants.py diff --git a/mocks/ginkgo/xmodule/__init__.py b/mocks/openedx/core/djangoapps/user_api/__init__.py similarity index 100% rename from mocks/ginkgo/xmodule/__init__.py rename to mocks/openedx/core/djangoapps/user_api/__init__.py diff --git a/mocks/ginkgo/xmodule/modulestore/__init__.py b/mocks/openedx/core/djangoapps/user_api/accounts/__init__.py similarity index 100% rename from mocks/ginkgo/xmodule/modulestore/__init__.py rename to mocks/openedx/core/djangoapps/user_api/accounts/__init__.py diff --git a/mocks/ginkgo/openedx/core/djangoapps/user_api/accounts/serializers.py b/mocks/openedx/core/djangoapps/user_api/accounts/serializers.py similarity index 100% rename from mocks/ginkgo/openedx/core/djangoapps/user_api/accounts/serializers.py rename to mocks/openedx/core/djangoapps/user_api/accounts/serializers.py diff --git a/mocks/hawthorn/__init__.py b/mocks/openedx/core/djangoapps/xmodule_django/__init__.py similarity index 100% rename from mocks/hawthorn/__init__.py rename to mocks/openedx/core/djangoapps/xmodule_django/__init__.py diff --git a/mocks/juniper/openedx/core/djangoapps/xmodule_django/models.py b/mocks/openedx/core/djangoapps/xmodule_django/models.py similarity index 100% rename from mocks/juniper/openedx/core/djangoapps/xmodule_django/models.py rename to mocks/openedx/core/djangoapps/xmodule_django/models.py diff --git a/mocks/ginkgo/openedx/core/release.py b/mocks/openedx/core/release.py similarity index 67% rename from mocks/ginkgo/openedx/core/release.py rename to mocks/openedx/core/release.py index 50d87913..e29bee84 100644 --- a/mocks/ginkgo/openedx/core/release.py +++ b/mocks/openedx/core/release.py @@ -2,5 +2,4 @@ Identify the release of Open edX platform """ -RELEASE_LINE = 'ginkgo' - +RELEASE_LINE = 'maple' diff --git a/mocks/hawthorn/course_modes/__init__.py b/mocks/xmodule/__init__.py similarity index 100% rename from mocks/hawthorn/course_modes/__init__.py rename to mocks/xmodule/__init__.py diff --git a/mocks/hawthorn/courseware/__init__.py b/mocks/xmodule/modulestore/__init__.py similarity index 100% rename from mocks/hawthorn/courseware/__init__.py rename to mocks/xmodule/modulestore/__init__.py diff --git a/mocks/juniper/xmodule/modulestore/django.py b/mocks/xmodule/modulestore/django.py similarity index 100% rename from mocks/juniper/xmodule/modulestore/django.py rename to mocks/xmodule/modulestore/django.py diff --git a/mocks/hawthorn/xmodule/modulestore/exceptions.py b/mocks/xmodule/modulestore/exceptions.py similarity index 100% rename from mocks/hawthorn/xmodule/modulestore/exceptions.py rename to mocks/xmodule/modulestore/exceptions.py diff --git a/pytest-ginkgo.ini b/pytest-ginkgo.ini deleted file mode 100644 index 0588a632..00000000 --- a/pytest-ginkgo.ini +++ /dev/null @@ -1,10 +0,0 @@ -# This file supports PyTest testing Figures against Ginkgo mocks - -[pytest] -DJANGO_SETTINGS_MODULE = devsite.test_settings - -norecursedirs = .* docs requirements - -python_paths = devsite mocks/ginkgo - -testpaths = ./tests diff --git a/pytest-hawthorn.ini b/pytest-hawthorn.ini deleted file mode 100644 index ef4e900b..00000000 --- a/pytest-hawthorn.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -DJANGO_SETTINGS_MODULE = devsite.test_settings - -norecursedirs = .* docs requirements -python_paths = devsite mocks/hawthorn -testpaths = ./tests diff --git a/pytest-juniper.ini b/pytest-juniper.ini deleted file mode 100644 index 3d7a1afb..00000000 --- a/pytest-juniper.ini +++ /dev/null @@ -1,10 +0,0 @@ -# This file supports PyTest testing Figures against Juniper mocks - -[pytest] -DJANGO_SETTINGS_MODULE = devsite.test_settings - -norecursedirs = .* docs requirements - -python_paths = devsite mocks/juniper - -testpaths = ./tests diff --git a/pytest.ini b/pytest.ini index ef4e900b..3484529f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,5 +2,5 @@ DJANGO_SETTINGS_MODULE = devsite.test_settings norecursedirs = .* docs requirements -python_paths = devsite mocks/hawthorn +python_paths = devsite mocks testpaths = ./tests diff --git a/tests/factories.py b/tests/factories.py index 90fddda5..0a11c89a 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -29,7 +29,7 @@ from figures.compat import StudentModule, CourseKeyField, GeneratedCertificate -from student.models import CourseAccessRole, CourseEnrollment, UserProfile +from common.djangoapps.student.models import CourseAccessRole, CourseEnrollment, UserProfile from lms.djangoapps.teams.models import CourseTeam, CourseTeamMembership import organizations @@ -45,11 +45,8 @@ SiteMauMetrics, ) -from tests.helpers import ( - organizations_support_sites, - OPENEDX_RELEASE, - GINKGO, -) +from tests.helpers import organizations_support_sites + import six @@ -76,7 +73,7 @@ class Meta: ['p','m','b','a','hs','jh','el','none', 'other',] ) profile_image_uploaded_at = fuzzy.FuzzyDateTime(datetime.datetime( - 2018,0o4,0o1, tzinfo=factory.compat.UTC)) + 2018,0o4,0o1, tzinfo=utc)) class UserFactory(DjangoModelFactory): @@ -90,7 +87,7 @@ class Meta: is_staff = False is_superuser = False date_joined = fuzzy.FuzzyDateTime(datetime.datetime( - 2018, 4, 1, tzinfo=factory.compat.UTC)) + 2018, 4, 1, tzinfo=utc)) # TODO: Figure out if this can be a SubFactory and the advantages profile = factory.RelatedFactory(UserProfileFactory, 'user') @@ -146,7 +143,7 @@ class Meta: if organizations_support_sites(): - class UserOrganizationMappingFactory(factory.DjangoModelFactory): + class UserOrganizationMappingFactory(DjangoModelFactory): class Meta(object): model = organizations.models.UserOrganizationMapping @@ -156,7 +153,7 @@ class Meta(object): is_amc_admin = False -class CourseOverviewFactory(factory.DjangoModelFactory): +class CourseOverviewFactory(DjangoModelFactory): class Meta: model = CourseOverview @@ -165,20 +162,18 @@ class Meta: COURSE_ID_STR_TEMPLATE.format(n))) display_name = factory.Sequence(lambda n: 'SFA Course {}'.format(n)) org = 'StarFleetAcademy' - - if not OPENEDX_RELEASE == GINKGO: - version = CourseOverview.VERSION + version = CourseOverview.VERSION display_org_with_default = factory.LazyAttribute(lambda o: o.org) created = factory.fuzzy.FuzzyDateTime(datetime.datetime( - 2018, 2, 1, tzinfo=factory.compat.UTC)) + 2018, 2, 1, tzinfo=utc)) enrollment_start = factory.fuzzy.FuzzyDateTime(datetime.datetime( - 2018, 3, 1, tzinfo=factory.compat.UTC)) + 2018, 3, 1, tzinfo=utc)) enrollment_end = factory.fuzzy.FuzzyDateTime(datetime.datetime( - 2018, 3, 15, tzinfo=factory.compat.UTC)) + 2018, 3, 15, tzinfo=utc)) start = factory.fuzzy.FuzzyDateTime(datetime.datetime( - 2018, 4, 1, tzinfo=factory.compat.UTC)) + 2018, 4, 1, tzinfo=utc)) end = factory.fuzzy.FuzzyDateTime(datetime.datetime( - 2018, 6, 1, tzinfo=factory.compat.UTC)) + 2018, 6, 1, tzinfo=utc)) self_paced = False @@ -216,9 +211,9 @@ class Meta: course_id = factory.Sequence(lambda n: as_course_key( COURSE_ID_STR_TEMPLATE.format(n))) created = fuzzy.FuzzyDateTime(datetime.datetime( - 2018,2,2, tzinfo=factory.compat.UTC)) + 2018,2,2, tzinfo=utc)) modified = fuzzy.FuzzyDateTime(datetime.datetime( - 2018,2,2, tzinfo=factory.compat.UTC)) + 2018,2,2, tzinfo=utc)) @classmethod @@ -235,57 +230,42 @@ def from_course_enrollment(cls, course_enrollment, **kwargs): return cls(**kwargs) -if OPENEDX_RELEASE == GINKGO: - class CourseEnrollmentFactory(DjangoModelFactory): - class Meta: - model = CourseEnrollment - - user = factory.SubFactory( - UserFactory, - ) - course_id = factory.SelfAttribute('course_overview.id') - course_overview = factory.SubFactory(CourseOverviewFactory) - created = factory.Sequence(lambda n: - (datetime.datetime(2018, 1, 1) + datetime.timedelta(days=n)).replace(tzinfo=utc)) - -else: - - class CourseEnrollmentFactory(DjangoModelFactory): - class Meta(object): - model = CourseEnrollment - - user = factory.SubFactory(UserFactory) - - created = factory.Sequence(lambda n: - (datetime.datetime(2018, 1, 1) + datetime.timedelta(days=n)).replace(tzinfo=utc)) - +class CourseEnrollmentFactory(DjangoModelFactory): + class Meta(object): + model = CourseEnrollment - @classmethod - def _create(cls, model_class, *args, **kwargs): - manager = cls._get_manager(model_class) - course_kwargs = {} - for key in kwargs.keys(): - if key.startswith('course__'): - course_kwargs[key.split('__')[1]] = kwargs.pop(key) - - if 'course' not in kwargs: - course_id = kwargs.get('course_id') - course_overview = None - if course_id is not None: - if isinstance(course_id, six.string_types): - course_id = as_course_key(course_id) - course_kwargs.setdefault('id', course_id) + user = factory.SubFactory(UserFactory) - try: - course_overview = CourseOverview.get_from_id(course_id) - except CourseOverview.DoesNotExist: - pass + created = factory.Sequence(lambda n: + (datetime.datetime(2018, 1, 1) + datetime.timedelta(days=n)).replace(tzinfo=utc)) - if course_overview is None: - course_overview = CourseOverviewFactory(**course_kwargs) - kwargs['course'] = course_overview - return manager.create(*args, **kwargs) + @classmethod + def _create(cls, model_class, *args, **kwargs): + manager = cls._get_manager(model_class) + course_kwargs = {} + for key in kwargs.keys(): + if key.startswith('course__'): + course_kwargs[key.split('__')[1]] = kwargs.pop(key) + + if 'course' not in kwargs: + course_id = kwargs.get('course_id') + course_overview = None + if course_id is not None: + if isinstance(course_id, six.string_types): + course_id = as_course_key(course_id) + course_kwargs.setdefault('id', course_id) + + try: + course_overview = CourseOverview.get_from_id(course_id) + except CourseOverview.DoesNotExist: + pass + + if course_overview is None: + course_overview = CourseOverviewFactory(**course_kwargs) + kwargs['course'] = course_overview + + return manager.create(*args, **kwargs) class CourseAccessRoleFactory(DjangoModelFactory): diff --git a/tests/helpers.py b/tests/helpers.py index 1a028f03..6a261aba 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -13,13 +13,16 @@ from organizations.models import Organization -# Ginkgo is the earliest supported platform -GINKGO = 'GINKGO' -HAWTHORN = 'HAWTHORN' - - def platform_release(): - return os.environ.get('OPENEDX_RELEASE', HAWTHORN) + """Identifies for which Open edX release we should test + This is to handle breaking changes between releases + + With the Maple upgrade, we've removed all Ginkgo handling, which in turn + removed all references to this function and OPENEDX_RELEASE + However, we will retain it "for now", at least until we upgrade post-Maple, + and decide if we retain this testing feature or remove it + """ + return os.environ.get('OPENEDX_RELEASE', 'MAPLE') OPENEDX_RELEASE = platform_release() diff --git a/tests/metrics/test_learner_course_grades.py b/tests/metrics/test_learner_course_grades.py index ff0a37d7..3ecfc97d 100644 --- a/tests/metrics/test_learner_course_grades.py +++ b/tests/metrics/test_learner_course_grades.py @@ -17,20 +17,13 @@ GeneratedCertificateFactory, ) -from tests.helpers import OPENEDX_RELEASE, GINKGO # Mock objects to test course and course section grade metrics -if OPENEDX_RELEASE == GINKGO: - from lms.djangoapps.grades.new.course_grade import ( - MockAggregatedScore, - MockSubsectionGrade, - ) -else: - from lms.djangoapps.grades.course_grade import ( - MockAggregatedScore, - MockSubsectionGrade, - ) +from lms.djangoapps.grades.course_grade import ( + MockAggregatedScore, + MockSubsectionGrade, + ) @pytest.mark.django_db @@ -39,10 +32,7 @@ class TestLearnerCourseGrades(object): @pytest.fixture(autouse=True) def setup(self, db): self.course_enrollment = CourseEnrollmentFactory() - if OPENEDX_RELEASE == GINKGO: - self.course_id = self.course_enrollment.course_id - else: - self.course_id = self.course_enrollment.course.id + self.course_id = self.course_enrollment.course.id self.lcg = LearnerCourseGrades(self.course_enrollment.user.id, self.course_id) diff --git a/tests/models/test_enrollment_data_model.py b/tests/models/test_enrollment_data_model.py index 501cc3c9..63125d41 100644 --- a/tests/models/test_enrollment_data_model.py +++ b/tests/models/test_enrollment_data_model.py @@ -33,11 +33,7 @@ OrganizationCourseFactory, SiteFactory, UserFactory) -from tests.helpers import ( - OPENEDX_RELEASE, - GINKGO, - organizations_support_sites -) +from tests.helpers import organizations_support_sites if organizations_support_sites(): from tests.factories import UserOrganizationMappingFactory @@ -111,7 +107,6 @@ def site_data(db, settings): return site_data -@pytest.mark.skipif(OPENEDX_RELEASE == GINKGO, reason='Breaks on CourseEnrollmentFactory') def test_set_enrollment_data_new_record(site_data): """Test we create a new EnrollmentData record @@ -134,7 +129,6 @@ def test_set_enrollment_data_new_record(site_data): assert obj.user == lcgm.user -@pytest.mark.skipif(OPENEDX_RELEASE == GINKGO, reason='Breaks on CourseEnrollmentFactory') def test_set_enrollment_data_update_existing(site_data): """Test we update an existing EnrollmentData record """ diff --git a/tests/models/test_enrollment_data_update_metrics.py b/tests/models/test_enrollment_data_update_metrics.py index e4ff46d9..5de3a6fb 100644 --- a/tests/models/test_enrollment_data_update_metrics.py +++ b/tests/models/test_enrollment_data_update_metrics.py @@ -53,7 +53,6 @@ def map_users_to_org(org, users): organization=org) for user in users] -# @pytest.mark.skipif(OPENEDX_RELEASE == GINKGO, reason='Breaks on CourseEnrollmentFactory') @pytest.mark.django_db class TestUpdateMetrics(object): """Test EnrollmentDataManager.update_metrics method diff --git a/tests/pipeline/test_course_daily_metrics.py b/tests/pipeline/test_course_daily_metrics.py index dc4b94db..d900a5c6 100644 --- a/tests/pipeline/test_course_daily_metrics.py +++ b/tests/pipeline/test_course_daily_metrics.py @@ -28,12 +28,8 @@ SiteFactory, StudentModuleFactory, ) +from tests.helpers import organizations_support_sites -from tests.helpers import ( - organizations_support_sites, - OPENEDX_RELEASE, - GINKGO, -) from six.moves import range @@ -86,12 +82,8 @@ class TestCourseDailyMetricsPipelineFunctions(object): def setup(self, db): self.today = datetime.date(2018, 6, 1) self.course_overview = CourseOverviewFactory() - if OPENEDX_RELEASE == GINKGO: - self.course_enrollments = [CourseEnrollmentFactory( - course_id=self.course_overview.id) for i in range(4)] - else: - self.course_enrollments = [CourseEnrollmentFactory( - course=self.course_overview) for i in range(4)] + self.course_enrollments = [CourseEnrollmentFactory( + course=self.course_overview) for i in range(4)] if organizations_support_sites(): self.my_site = SiteFactory(domain='my-site.test') @@ -276,10 +268,7 @@ def setup(self, db): def test_load(self, monkeypatch): # pick a course, any course (we'll just pick the first, but doesn't matter which) - if OPENEDX_RELEASE == GINKGO: - course_id = self.course_enrollments[0].course_id - else: - course_id = self.course_enrollments[0].course.id + course_id = self.course_enrollments[0].course.id def get_data(self, date_for, ed_next=False): return { diff --git a/tests/pipeline/test_course_mau.py b/tests/pipeline/test_course_mau.py index 8923b11c..ae631707 100644 --- a/tests/pipeline/test_course_mau.py +++ b/tests/pipeline/test_course_mau.py @@ -9,6 +9,7 @@ import pytest from mock import Mock +from django.utils.timezone import utc from factory import fuzzy from figures.pipeline.mau_pipeline import ( @@ -45,8 +46,8 @@ def create_student_module_recs(course_id): # Create SM in our month year_for = 2020 month_for = 1 - start_dt = datetime(year_for, month_for, 1, tzinfo=fuzzy.compat.UTC) - end_dt = datetime(year_for, month_for, 31, tzinfo=fuzzy.compat.UTC) + start_dt = datetime(year_for, month_for, 1, tzinfo=utc) + end_dt = datetime(year_for, month_for, 31, tzinfo=utc) date_gen = fuzzy.FuzzyDateTime(start_dt=start_dt, end_dt=end_dt) in_range = [StudentModuleFactory(created=start_dt, @@ -54,9 +55,9 @@ def create_student_module_recs(course_id): course_id=course_id) for i in range(3)] # Create a rec before - before_date = datetime(2019, 12, 31, tzinfo=fuzzy.compat.UTC) + before_date = datetime(2019, 12, 31, tzinfo=utc) # Create a rec after - after_date = datetime(2020, 2, 1, tzinfo=fuzzy.compat.UTC) + after_date = datetime(2020, 2, 1, tzinfo=utc) out_range = [ StudentModuleFactory(created=before_date, modified=before_date, diff --git a/tests/tasks/test_daily_tasks.py b/tests/tasks/test_daily_tasks.py index 41b29347..471d39ab 100644 --- a/tests/tasks/test_daily_tasks.py +++ b/tests/tasks/test_daily_tasks.py @@ -77,7 +77,7 @@ CourseOverviewFactory, SiteDailyMetricsFactory, SiteFactory) -from tests.helpers import OPENEDX_RELEASE, GINKGO, FakeException, fake_course_key +from tests.helpers import FakeException, fake_course_key @pytest.mark.parametrize('extra_params', [{}, {'ed_next': True}]) @@ -189,8 +189,6 @@ def fake_update_enrollment_data_for_course(course_id): assert set(collected_course_ids) == set(course_ids) -@pytest.mark.skipif(OPENEDX_RELEASE == GINKGO, - reason='Apparent Django 1.8 incompatibility') @pytest.mark.parametrize('extra_params', [{}, {'ed_next': True}]) def test_populate_daily_metrics_for_site_error_on_cdm(transactional_db, monkeypatch, @@ -230,8 +228,6 @@ def fake_update_enrollment_data_for_course(course_id): assert last_log.message == expected_msg -@pytest.mark.skipif(OPENEDX_RELEASE == GINKGO, - reason='Apparent Django 1.8 incompatibility') @pytest.mark.parametrize('extra_params', [{}, {'ed_next': True}]) def test_populate_daily_metrics_for_site_site_dne(transactional_db, monkeypatch, @@ -256,8 +252,6 @@ def test_populate_daily_metrics_for_site_site_dne(transactional_db, assert last_log.message == expected_message.format(bad_site_id) -@pytest.mark.skipif(OPENEDX_RELEASE == GINKGO, - reason='Apparent Django 1.8 incompatibility') @pytest.mark.parametrize('func', [ populate_daily_metrics, populate_daily_metrics_next ]) @@ -303,8 +297,6 @@ def fake_populate_daily_metrics_for_site(site_id, **_kwargs): # TODO: def test_populate_daily_metrics_future_date_error -@pytest.mark.skipif(OPENEDX_RELEASE == GINKGO, - reason='Apparent Django 1.8 incompatibility') def test_populate_daily_metrics_enrollment_data_error(transactional_db, monkeypatch, caplog): @@ -336,8 +328,6 @@ def fake_update_enrollment_data_fails(**kwargs): assert last_log.message == expected_msg -@pytest.mark.skipif(OPENEDX_RELEASE == GINKGO, - reason='Broken test. Apparent Django 1.8 incompatibility') @pytest.mark.parametrize('func', [ populate_daily_metrics, populate_daily_metrics_next ]) diff --git a/tests/tasks/test_monthly_tasks.py b/tests/tasks/test_monthly_tasks.py index 742fb08e..00aef923 100644 --- a/tests/tasks/test_monthly_tasks.py +++ b/tests/tasks/test_monthly_tasks.py @@ -11,7 +11,7 @@ run_figures_monthly_metrics) from tests.factories import SiteFactory -from tests.helpers import OPENEDX_RELEASE, GINKGO, FakeException +from tests.helpers import FakeException def test_populate_monthly_metrics_for_site(transactional_db, monkeypatch): @@ -93,8 +93,6 @@ def fake_populate_monthly_metrics_for_site(celery_task_group): assert set(sites_visited) == set([rec.id for rec in expected_sites]) -@pytest.mark.skipif(OPENEDX_RELEASE == GINKGO, - reason='Broken test. Apparent Django 1.8 incompatibility') def test_run_figures_monthly_metrics_with_unfaked_subtask(transactional_db, monkeypatch): """Verify we visit the function our subtasks calls diff --git a/tests/test_apps.py b/tests/test_apps.py index ec1f2c73..fc0d9c1e 100644 --- a/tests/test_apps.py +++ b/tests/test_apps.py @@ -6,21 +6,12 @@ import mock import pytest -from tests.helpers import OPENEDX_RELEASE, GINKGO - - -class AwsSettingsType(object): - AWS = u'aws' - class ProdSettingsType(object): PRODUCTION = u'production' -@pytest.mark.skipif(OPENEDX_RELEASE == GINKGO, - reason='Plugins not supported in Ginkgo') @pytest.mark.parametrize('klass, expected_val', [ - (AwsSettingsType, AwsSettingsType.AWS), (ProdSettingsType, ProdSettingsType.PRODUCTION), ]) def test_production_settings_name(klass, expected_val): diff --git a/tests/test_compat.py b/tests/test_compat.py index d4f54611..4de4e48d 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -62,73 +62,8 @@ def patch_module(module_path, extra_properties=None): # TODO: Test with no release line - -@patch('openedx.core.release.RELEASE_LINE', 'ginkgo') -def test_compat_module_with_ginkgo(): - """Test Ginkgo compat path works - - Yes, we have a number of assertions here. We are testing the state of - `figures.compat` when we expect Figures to run in Ginkgo. Running in one - test also saves execution time as we need to ste up the whole module to - import as Ginkgo even if we are only testing one object in the module - """ - with patch_module('lms.djangoapps.grades.new.course_grade_factory', - {'CourseGradeFactory': 'cgf'}): - with patch_module('certificates.models', {'GeneratedCertificate': 'gc'}): - with patch_module('courseware.models', {'StudentModule': 'sm'}): - with patch_module('courseware.courses', {'get_course_by_id': 'gcbid'}): - with patch_module('openedx.core.djangoapps.xmodule_django.models', - {'CourseKeyField': 'ckf'}): - import figures.compat - reload(figures.compat) - assert figures.compat.RELEASE_LINE == 'ginkgo' - assert hasattr(figures.compat, 'CourseGradeFactory') - assert figures.compat.CourseGradeFactory == 'cgf' - assert hasattr(figures.compat, 'GeneratedCertificate') - assert figures.compat.GeneratedCertificate == 'gc' - assert hasattr(figures.compat, 'StudentModule') - assert figures.compat.StudentModule == 'sm' - assert hasattr(figures.compat, 'get_course_by_id') - assert figures.compat.get_course_by_id == 'gcbid' - assert hasattr(figures.compat, 'CourseKeyField') - assert figures.compat.CourseKeyField == 'ckf' - - -@patch('openedx.core.release.RELEASE_LINE', 'hawthorn') -def test_release_line_with_hawthorn(): - """Test Hawthorn compat path works - - Yes, we have a number of assertions here. We are testing the state of - `figures.compat` when we expect Figures to run in Ginkgo. Running in one - test also saves execution time as we need to ste up the whole module to - import as Ginkgo even if we are only testing one object in the module - """ - with patch_module('lms.djangoapps.grades.course_grade_factory', - {'CourseGradeFactory': 'cgf'}): - with patch_module('lms.djangoapps.certificates.models', - {'GeneratedCertificate': 'gc'}): - with patch_module('courseware.models', {'StudentModule': 'sm'}): - with patch_module('courseware.courses', - {'get_course_by_id': 'gcbid'}): - with patch_module('opaque_keys.edx.django.models', - {'CourseKeyField': 'ckf'}): - import figures.compat - reload(figures.compat) - assert figures.compat.RELEASE_LINE == 'hawthorn' - assert hasattr(figures.compat, 'CourseGradeFactory') - assert figures.compat.CourseGradeFactory == 'cgf' - assert hasattr(figures.compat, 'GeneratedCertificate') - assert figures.compat.GeneratedCertificate == 'gc' - assert hasattr(figures.compat, 'StudentModule') - assert figures.compat.StudentModule == 'sm' - assert hasattr(figures.compat, 'get_course_by_id') - assert figures.compat.get_course_by_id == 'gcbid' - assert hasattr(figures.compat, 'CourseKeyField') - assert figures.compat.CourseKeyField == 'ckf' - - -@patch('openedx.core.release.RELEASE_LINE', 'juniper') -def test_release_line_with_juniper(): +@patch('openedx.core.release.RELEASE_LINE', 'maple') +def test_release_line_with_maple(): """Test Hawthorn compat path works Yes, we have a number of assertions here. We are testing the state of @@ -148,7 +83,7 @@ def test_release_line_with_juniper(): {'CourseKeyField': 'ckf'}): import figures.compat reload(figures.compat) - assert figures.compat.RELEASE_LINE == 'juniper' + assert figures.compat.RELEASE_LINE == 'maple' assert hasattr(figures.compat, 'CourseGradeFactory') assert figures.compat.CourseGradeFactory == 'cgf' assert hasattr(figures.compat, 'GeneratedCertificate') diff --git a/tox.ini b/tox.ini index 498a81fc..0b2a5b8e 100644 --- a/tox.ini +++ b/tox.ini @@ -10,13 +10,10 @@ [tox] envlist = - py27-ginkgo - py27-hawthorn - py27-hawthorn_multisite - py35-juniper_community - py35-juniper_multisite + py38-maple_community + # py38-maple_multisite lint - edx_lint_check + # edx_lint_check skip_missing_interpreters=true @@ -24,11 +21,8 @@ skip_missing_interpreters=true [testenv] deps = - ginkgo: -r{toxinidir}/devsite/requirements/ginkgo.txt - hawthorn: -r{toxinidir}/devsite/requirements/hawthorn.txt - hawthorn_multisite: -r{toxinidir}/devsite/requirements/hawthorn_multisite.txt - juniper_community: -r{toxinidir}/devsite/requirements/juniper_community.txt - juniper_multisite: -r{toxinidir}/devsite/requirements/juniper_multisite.txt + maple_community: -r{toxinidir}/devsite/requirements/community.txt + maple_multisite: -r{toxinidir}/devsite/requirements/multisite.txt -r{toxinidir}/devsite/requirements/test.txt whitelist_externals = @@ -36,42 +30,36 @@ whitelist_externals = edx_lint_check setenv = - DJANGO_SETTINGS_MODULE = devsite.test_settings PYTHONPATH = {toxinidir} - ginkgo: OPENEDX_RELEASE = GINKGO - hawthorn: OPENEDX_RELEASE = HAWTHORN - hawthorn_multisite: OPENEDX_RELEASE = HAWTHORN - juniper_community: OPENEDX_RELEASE = JUNIPER - juniper_multisite: OPENEDX_RELEASE = JUNIPER + maple_community: OPENEDX_RELEASE = MAPLE + maple_multisite: OPENEDX_RELEASE = MAPLE commands = - ginkgo: pytest -c pytest-ginkgo.ini {posargs} - hawthorn: pytest -c pytest-hawthorn.ini {posargs} - hawthorn_multisite: pytest -c pytest-hawthorn.ini {posargs} - juniper_community: pytest -c pytest-juniper.ini {posargs} - juniper_multisite: pytest -c pytest-juniper.ini {posargs} + maple_community: pytest {posargs} + maple_multisite: pytest {posargs} + [testenv:lint] -basepython=python2 +basepython=python3 deps = - -r{toxinidir}/devsite/requirements/hawthorn.txt + -r{toxinidir}/devsite/requirements/community.txt commands = flake8 figures devsite pylint --load-plugins pylint_django ./figures [testenv:edx_lint_check] -basepython=python2 +basepython=python3 deps = - -r{toxinidir}/devsite/requirements/hawthorn.txt + -r{toxinidir}/devsite/requirements/community.txt commands = edx_lint write pylintrc echo "If this fails, then you need to run '$ tox -e write_edx_lint' locally" git diff --exit-code # Ensure pylintrc is up to date [testenv:write_edx_lint] -basepython=python2 +basepython=python3 deps = - -r{toxinidir}/devsite/requirements/hawthorn.txt + -r{toxinidir}/devsite/requirements/community.txt commands = edx_lint write pylintrc