Django App is detected by Tutor but the URLs Doesn't Work

I recently created a Django App that is integrated into my Tutor installed Open Edx instance.
In order to integrate it, I followed the instructions of using a Django App Plugin.

Then when I run the following commands:

tutor local run lms ./manage.py lms shell  

>>> from django.apps import apps
>>> [app.verbose_name for app in apps.get_app_configs()]

it displays my django app on the list, indicating that the Open Edx Tutor now detects it after multiple attempts of troubleshooting.

But here’s the problem.
In my django app, I created a views subfolder with an __init.py and views.py with this on it:

from django.shortcuts import render

def features_dashboard(request):
    return render(request, 'features_dashboard.html')

then in my urls.py, I added this:

from django.conf.urls import url
from .views import views

urlpatterns = [
    url(r'^features/$', views.features_dashboard, name='features_dashboard'),
]

Then in my templates, I created a new html file just to indicate a Welcome Page on it.

Now when I go to my openedx instance via my LMS_DOMAIN by https://my-domain/features/

It displays this:

It’s quite hard to detect the problem since my app has been detected by Open Edx successfully yet my URLs remain undetected. But as indicated in the instructions that I followed, I wouldn’t have a problem regarding the URLs integration. But now, it is my problem. I definitely need help right now.

What are your entry point and plugin configs? Did you do the step 2 and step 4 from the guide that you mentioned?

These are my configurations in apps.py:


"""
coursebank_features Django application initialization.
"""

from django.apps import AppConfig


class CoursebankFeaturesConfig(AppConfig):
    """
    Configuration for the coursebank_features Django application.
    """

    name = 'coursebank_features'

    plugin_app = {
        'url_config': {
            'lms.djangoapp': {
                'namespace': 'my_app',
                'regex': '^api/my_app/',
                'relative_path': 'urls',
            }
        },
        'settings_config': {
            'lms.djangoapp': {
                'production': { 'relative_path': 'settings.production' },
                'common': { 'relative_path': 'settings.common' },
            }
        },
        'signals_config': {
            'lms.djangoapp': {
                'relative_path': 'my_signals',
                'receivers': [{
                    'receiver_func_name': 'on_signal_x',
                    'signal_path': 'full_path_to_signal_x_module.SignalX',
                    'dispatch_uid': 'my_app.my_signals.on_signal_x',
                    'sender_path': 'full_path_to_sender_app.ModelZ',
                }],
            }
        },
        'view_context_config': {
            'lms.djangoapp': {
                'course_dashboard': 'my_app.context_api.get_dashboard_context'
            }
        },
    }

and in my setup.py:

#!/usr/bin/env python
"""
Package metadata for coursebank_features.
"""
import os
import re
import sys

from setuptools import find_packages, setup


def get_version(*file_paths):
    """
    Extract the version string from the file.

    Input:
     - file_paths: relative path fragments to file with
                   version string
    """
    filename = os.path.join(os.path.dirname(__file__), *file_paths)
    version_file = open(filename, encoding="utf8").read()
    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
                              version_file, re.M)
    if version_match:
        return version_match.group(1)
    raise RuntimeError('Unable to find version string.')


def load_requirements(*requirements_paths):
    """
    Load all requirements from the specified requirements files.

    Requirements will include any constraints from files specified
    with -c in the requirements files.
    Returns a list of requirement strings.
    """
    requirements = {}
    constraint_files = set()

    # groups "pkg<=x.y.z,..." into ("pkg", "<=x.y.z,...")
    requirement_line_regex = re.compile(r"([a-zA-Z0-9-_.]+)([<>=][^#\s]+)?")

    def add_version_constraint_or_raise(current_line, current_requirements, add_if_not_present):
        regex_match = requirement_line_regex.match(current_line)
        if regex_match:
            package = regex_match.group(1)
            version_constraints = regex_match.group(2)
            existing_version_constraints = current_requirements.get(package, None)
            # fine to add constraints to an unconstrained package,
            # raise an error if there are already constraints in place
            if existing_version_constraints and existing_version_constraints != version_constraints:
                raise BaseException(f'Multiple constraint definitions found for {package}:'
                                    f' "{existing_version_constraints}" and "{version_constraints}".'
                                    f'Combine constraints into one location with {package}'
                                    f'{existing_version_constraints},{version_constraints}.')
            if add_if_not_present or package in current_requirements:
                current_requirements[package] = version_constraints

    # read requirements from .in
    # store the path to any constraint files that are pulled in
    for path in requirements_paths:
        with open(path) as reqs:
            for line in reqs:
                if is_requirement(line):
                    add_version_constraint_or_raise(line, requirements, True)
                if line and line.startswith('-c') and not line.startswith('-c http'):
                    constraint_files.add(os.path.dirname(path) + '/' + line.split('#')[0].replace('-c', '').strip())

    # process constraint files: add constraints to existing requirements
    for constraint_file in constraint_files:
        with open(constraint_file) as reader:
            for line in reader:
                if is_requirement(line):
                    add_version_constraint_or_raise(line, requirements, False)

    # process back into list of pkg><=constraints strings
    constrained_requirements = [f'{pkg}{version or ""}' for (pkg, version) in sorted(requirements.items())]
    return constrained_requirements


def is_requirement(line):
    """
    Return True if the requirement line is a package requirement.

    Returns:
        bool: True if the line is not blank, a comment,
        a URL, or an included file
    """
    return line and line.strip() and not line.startswith(("-r", "#", "-e", "git+", "-c"))


VERSION = get_version('coursebank_features', '__init__.py')

if sys.argv[-1] == 'tag':
    print("Tagging the version on github:")
    os.system("git tag -a %s -m 'version %s'" % (VERSION, VERSION))
    os.system("git push --tags")
    sys.exit()

README = open(os.path.join(os.path.dirname(__file__), 'README.rst'), encoding="utf8").read()
CHANGELOG = open(os.path.join(os.path.dirname(__file__), 'CHANGELOG.rst'), encoding="utf8").read()

setup(
    name='coursebank-features',
    version=VERSION,
    description="""One-line description for README and other doc files.""",
    long_description=README + '\n\n' + CHANGELOG,
    author='edX',
    author_email='oscm@edx.org',
    url='https://github.com/openedx/coursebank-features',
    packages=find_packages(
        include=['coursebank_features', 'coursebank_features.*'],
        exclude=["*tests"],
    ),
    
    entry_points={
        "lms.djangoapp": [
            "my_app = coursebank_features.apps:CoursebankFeaturesConfig",
        ],
        "cms.djangoapp": [
        ],
    },

    include_package_data=True,
    install_requires=load_requirements('requirements/base.in'),
    python_requires=">=3.8",
    license="AGPL 3.0",
    zip_safe=False,
    keywords='Python edx',
    classifiers=[
        'Development Status :: 3 - Alpha',
        'Framework :: Django',
        'Framework :: Django :: 3.2',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
        'Natural Language :: English',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.8',
    ],
)

Then as indicated in the 5th step here, for Plugin Settings, I inserted the following function into each of the Plugin Settings modules that I created in the /settings folder:

def plugin_settings(settings):
    # Update the provided settings module with any app-specific settings.
    # For example:
    #     settings.FEATURES['ENABLE_MY_APP'] = True
    #     settings.MY_APP_POLICY = 'foo'

So step 1 would be checking your Django config and make sure that your app is present in the INSTALLED_APPS list. Second, you should remove regex option from url_config in your apps.py, I think this is what is messing with your urls.

I was tackling these issues myself last week so I decided to write a small guide with the example of config that worked for me, maybe this will help also https://medium.com/p/152b3ef2a33e

1 Like

Wow! Thank you for the guide and your answer. I’ve been searching online for some guide yet I haven’t found your blog yet. I will try to explore this further. Hoping it would work and will reply here if some problems occur.

1 Like

Hello @dariadaria! I’ve followed your instructions here at https://medium.com/p/152b3ef2a33e and I would like to raise some concerns.
For example, you’ve indicated there that I should
Add the app to the Open Edx platform that is running in tutor development mode

shoudl I add it there even I will be using it in production (local)?

Well if not, then whoy doesn’t it work when I add it in production mode directly? Here are my volume mapping configurations in docker-compose.override.yml:

version: "3.7"
services:
  lms:
    volumes:
    - .local/share/tutor/env/build/openedx/requirements/coursebank-features:/mnt/coursebank-features

It says
python: can't open file 'setup.py': [Errno 2] No such file or directory
after running

docker exec -it tutor_dev_lms_1 bash
cd /mnt/coursebank-features
python setup.py develop

The tutor development part is for people who run their open edx in the dev mode, if you run only local mode you can skip this step.

If you wanna connect your app with the volume to local mode you can try, I haven’t tested it, I can not guarantee it will work :slight_smile: But the problem you described appears to be because you specified the wrong path in your docker-compose.override.yml, the path to the directory with the app should be absolute.

In any case, I do recommend using tutor dev mode when you developing new features as it gives you more tools to debug the problem, like showing the full stack trace, etc.

Thank you for your reply. I managed to add it succesfully to dev mode.

My django app is in this directory /home/ubuntu/.local/share/tutor/env/build/openedx/requirements/coursebank-features but I only indicated .local/share/tutor/env/build/openedx/requirements/coursebank-features in the volume mapping configurations.

But sadly, the problem is still there. My URL for features still doesn’t load.
When I go to https://my-domain/features/, it still returns the same error.

UPDATE:

It’s working now. I just realized I was using the wrong URL. Silly of me. :sweat_smile:

The url I should be using was the regex that I have indicated in my apps.py. instead of just typing the name of the app in the URL.