Setting up CKEditor package (WYSIWYG feature) for Admin Dashboard

I’m trying to add a WYSIWYG editor for my Django admin users for the Dashboard. I settled on CKEditor. Here are the installation instructions on Read The Docs: Django CKEditor — Django CKEditor 6.5.1 documentation

Under the “Required” installation steps heading, I followed each step.

To summarize:

  1. I ran: $ pip install django-ckeditor
  2. I added ckeditor to INSTALLED_APPS list in settings.py.
  3. I ran $ python manage.py collectstatic
  4. I added this variable declaration also in my settings configuration file at the bottom: CKEDITOR_BASEPATH = "/my_static/ckeditor/ckeditor/". I created a custom admin folder inside the top level project templates directory and then inside that admin folder I created a file and added:
{% extends "admin/change_form.html" %}
{% block extrahead %}
<script>window.CKEDITOR_BASEPATH = '/my_static/ckeditor/ckeditor/';</script>
{{ block.super }}
{% endblock %}

I saved the above changes. I restarted my local Django dev server. When I open the Admin Dashboard and add content, the WYSIWYG editor doesn’t appear. There are no errors or traceback.

In the instructions laid out above, there were additional steps, but they are all marked as “optional” and mostly for file uploads which I don’t require at this point.

What might I be missing?

Are you actually using a directory named my_static as your STATIC_URL?

Hi @KenWhitesell! Thank you for the reply.

I can’t believe I missed that. I have now updated my settings.py with the correct CKEDITOR_BASEPATH variable and custom Admin template as well.

Here is my /templates/admin/change_form.html:

{% extends "admin/change_form.html" %}

{% block extrahead %}
<script>window.CKEDITOR_BASEPATH = '/staticfiles/ckeditor/ckeditor/';</script>
{{ block.super }}
{% endblock %}

At the bottom of my settings.py, it now shows this:

CKEDITOR_BASEPATH = "/staticfiles/ckeditor/ckeditor/"

Here is some additional information where I declare all the static variables in my settings.py:

STATIC_URL = '/staticfiles/'
STATICFILES_DIRS = [ os.path.join(BASE_DIR,'static') ]
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
# I got the below from:
# https://stackoverflow.com/questions/53859972/django-whitenoise-500-server-error-in-non-debug-mode
# STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage"
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'

I saved my changes and restarted my Django server. When I go to add content in the Admin Dashboard, the WYSIWYG editor still does not appear.

What other information could I provide to help troubleshoot?

Please post your INSTALLED_APPS and TEMPLATES settings.

In your console log, are you seeing any GET requests for ckeditor?

From settings.py here is the information you’ve requested:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR,'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
INSTALLED_APPS = [
    'contents.apps.ContentsConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'ckeditor',
]

Here is a sample of console log:

Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
November 21, 2022 - 13:09:35
Django version 4.1.1, using settings 'hypno_juicer.settings'
Starting development server at http://127.0.0.1:9000/
Quit the server with CONTROL-C.
[21/Nov/2022 13:09:45] "GET /admin/contents/preamble/4/change/ HTTP/1.1" 200 9430
[21/Nov/2022 13:09:45] "GET /admin/jsi18n/ HTTP/1.1" 200 3343
[21/Nov/2022 13:15:32] "GET /admin/contents/research/ HTTP/1.1" 200 8950
[21/Nov/2022 13:15:32] "GET /admin/jsi18n/ HTTP/1.1" 200 3343

There are GET requests. Some I can identify as Admin Dashboard activity but the ones with jsi18n I don’t recognize. Not sure if those are anonymous/hash CKEditor calls or if they are some other basic and typical routine Django GET requests.

The jsi18n references are from the original change_form.html, so they’re part of the admin.

The next thing you might want to check is to see if that line of code (the <script> tag) was injected into the page. That’ll help determine if the admin is picking up your modified change_form template or not. (I’m guessing not because there’s no attempt to get the editor, but it’s worth verifying.)

You might also want to move the ckeditor in your INSTALLED apps above the django. packages (but after your contents. entry).

Finally, what does your model look like that you’re expecting to use this? (Is the field defined as a RichTextField?) Or defined as being used in your ModelAdmin class?

This is a great idea. Although since I wasn’t sure how to test whether the custom admin change_form.html code extension is being injected properly, I went to Google and tried searching for combinations of terms such as: ‘test inject partial template django’ as well as ‘test inject template django’. That turned up lots of interesting search results involving Django and TDD in general but I couldn’t locate a guide or SO question for testing whether custom admin script is successfully injected. @KenWhitesell: How might you recommend I check that my code was injected properly?

Another relevant variable at play here is perhaps where I may have (mis)placed my custom admin injection. Below is the file tree for my project. As you can see, the change_form.html file is positioned at: templates/admin/change_form.html. Is that the right place to put it?

Noted. I have made this adjustment. Here is my INSTALLED_APPS list variable inside my settings.py now:

INSTALLED_APPS = [
    'contents.apps.ContentsConfig',
    'ckeditor',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Bingo! There is a fantastic insight. Up to this point I haven’t used a RichTextField. As a matter of fact when I first wrote this thread I tried the RichTextField and immediately noticed that it’s not in the official Django docs. It’s not supported as an official model. But here I am 2 weeks later and I found a guide on Geeks for Geeks demonstrating how to install and get CKEditor running and they show how it’s essential to reference RichTextField at the top of the models.py from the (unofficial) CKEditor fields package. Included here is my latest attempt at getting CKEditor working:

class ScriptSuggestion(models.Model):
    title = models.CharField(max_length=300,blank=True)
    body = models.TextField(max_length=300000,blank=True)
    geeks_field = RichTextField(config_name='default',max_length=300000,blank=True)
    author = models.CharField(max_length=300,blank=True)
    slug = models.SlugField(unique=True,blank=True)

    def __str__(self):
        return f'{self.title}'

Take note of the geeks_field class attribute. Of course I have now also included at the top of my models.py the proper reference to RichTextField:

from ckeditor.fields import RichTextField

Other than this change, I’ve completed all the other steps in that Geeks for Geeks guide. I migrated the db and now in the Admin Dashboard, when I go to add content, there is a RichTextField! Although it’s blocked out. There is no way for me to enter content. Here is a sample screenshot of what my Admin Dashboard looks like:

As you can see in the screenshot, there is a new entry for “Geeks field” but I can’t enter text and there are no WYSIWYG options showing yet.

Could this be caused by the custom admin injection being misconfigured (as I discussed above)? The next necessary step is to test the template injection which I am not sure how to do. Any guidance here would help.

As per the Geeks for Geeks guide and in the official CKEditor doc, I have also now included this in my settings.py:

CKEDITOR_BASEPATH = "/staticfiles/ckeditor/ckeditor/"

CKEDITOR_CONFIGS = {
    'default': {
        'toolbar': 'full',
        'height': 300,
        'width': 300,
    },
}

Use your developer’s tools in the browser to examine the html that was sent to the browser to see if it’s there.

I believe so? (Looks right, possibly the easiest way to tell would be to add something to the form - something that you can obviously find either looking at the form on the page or in the source of the page in your browser.)

Correct, it’s part of the CKEditor extension. (It’s probably worth your time to take a look at it’s implementation at https://github.com/django-ckeditor/django-ckeditor/blob/master/ckeditor/fields.py)

Possibly - but with all the changes you’ve made, it’s worth going back to step one.

Start with looking at the http requests being sent when the page is requested. You may also want to clear the browser’s cache and/or do a shift-reload on the browser to ensure that it tries to load the extension.
Also look at the network tab in your browser’s developer tools to see if the request for that extension is being made.

Hi Ken! Thanks for the reply. I have more information to share. We’re making progress but we aren’t quite at the finish line yet.

This is a great idea and a lot easier than I thought it was going to be. Inside my custom admin extender partial template, I inserted a random string of characters within the extra head block. It looks like this now:

{% extends "admin/change_form.html" %}

{% block extrahead %}
<script>window.CKEDITOR_BASEPATH = '/staticfiles/ckeditor/ckeditor/';</script>
{{ block.super }}
testasdf
{% endblock %}

Take notice of ‘testasdf’. When I navigated to my Admin Dashboard, sure enough, there it was:

image

You can see ‘testasdf’ included above the yellow “Django administration” banner. Ken also recommended searching Developer tools. I’ve used Chrome’s Developer tools extensively in the past for exploring and playing with CSS properties and values, but I am not familiar with how to use it to check or analyze for CKEditor data. I have 0 knowledge of JavaScript. I have a loose understanding of the network tool, but I wouldn’t know how to use it in this situation.

I managed to collect additional helpful information by other means

When I navigate to view-source:// in Chrome’s address bar on the Django Admin Dashboard for this app, I can see CKEditor JavaScript and other libraries are being correctly injected in the header:

<script>window.CKEDITOR_BASEPATH = '/staticfiles/ckeditor/ckeditor/';</script>

<script src="/admin/jsi18n/"></script>
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/ckeditor/ckeditor-init.js" data-ckeditor-basepath="/staticfiles/ckeditor/ckeditor/" id="ckeditor-init-script"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<script src="/static/ckeditor/ckeditor/ckeditor.js"></script>
<script src="/static/admin/js/core.js"></script>
<script src="/static/admin/js/admin/RelatedObjectLookups.js"></script>
<script src="/static/admin/js/actions.js"></script>
<script src="/static/admin/js/urlify.js"></script>
<script src="/static/admin/js/prepopulate.js"></script>
<script src="/static/admin/js/vendor/xregexp/xregexp.js"></script>

testasdf

As you can see, testasdf is included at the bottom so that doubly verifies the injection is active. You can also see ckeditor.js is present, along with ckeditor-init.js. So at least some of this is working. What kind of raises a flag in my mind is at the top where it says: <script>window.CKEDITOR_BASEPATH = '/staticfiles/ckeditor/ckeditor/';</script>, Is that parsing correctly? Or should that Jinja syntax be parsed differently? There is something not right about the way window.CKEDITOR_BASEPATH looks to me.

There is more.

I am getting 404 requests in my shell:

$ python manage.py runserver 9000
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
December 04, 2022 - 18:35:47
Django version 4.1.1, using settings 'hypno_juicer.settings'
Starting development server at http://127.0.0.1:9000/
Quit the server with CONTROL-C.
[04/Dec/2022 18:35:51] "GET /admin/contents/scriptsuggestion/23/change/ HTTP/1.1" 200 11029
[04/Dec/2022 18:35:51] "GET /admin/jsi18n/ HTTP/1.1" 200 3343
Not Found: /staticfiles/ckeditor/ckeditor/config.js
[04/Dec/2022 18:35:51] "GET /staticfiles/ckeditor/ckeditor/config.js?t=M6K9 HTTP/1.1" 404 3613
Not Found: /staticfiles/ckeditor/ckeditor/lang/en.js
Not Found: /staticfiles/ckeditor/ckeditor/skins/moono-lisa/editor.css
[04/Dec/2022 18:35:51] "GET /staticfiles/ckeditor/ckeditor/skins/moono-lisa/editor.css?t=M6K9 HTTP/1.1" 404 3667
[04/Dec/2022 18:35:51] "GET /staticfiles/ckeditor/ckeditor/lang/en.js?t=M6K9 HTTP/1.1" 404 3616

These 404’s are pointing to my staticfiles directory. I have that directory properly configured. The CKEditor project doc recommends running collectstatic, which I did when I first tried setting it up a few weeks ago. Today I ran the collect static command a second time, just to make sure. In my development environment, I can see all the automatically generated files. Here is a small sample:

I’d like to draw attention to the second last entry listed in this file tree. It is: config.js. This is one of the files in the traceback above which Django is saying doesn’t exist (404). What does this mean? Why is Django saying it isn’t serving /staticfiles/ckeditor/ckedtiro/config.js when it clearly exists in the right location?

(btw: I have ensured that the staticfiles directory is included in my .gitignore source code which is why all the static files are greyed out. But even though they are grey, they still all exist and Django should be able to serve them in this state.)

I think we’re getting closer here - what is your STATICFILES_DIRS setting? (My current thinking is that you don’t have runserver configured for the staticfiles app to look in your staticfiles directory for static files.)

Your other static files are being served from the static directory - you may want to install these components in your static directory and change your configuration setting accordingly.

The issue is becoming clearer to me too. I’m going to start off with a short rant which will seem like a tangent but then we will continue troubleshooting my static files configuration.

Setting up static files properly has been a pain point for me in the past for as long as I have been using Django (as a hobbiest not a professional developer).

My understanding of Django’s architecture, each web app should have its own static files in development. Then when deployed to production, collectstatic is invoked to gather all the static folders from all the individual web apps and then put them in one place for the web server to handle. That makes sense. But when setting up the static file configuration, when I have a question, different developers on the web on Stack Overflow or teachers in different Udemy courses, all use different static file configurations. So if one proposed solution I find online doesn’t work, I then proceed to try a different static configuration until I get it to work. After having run collect static a few times, it’s too late because now my source code has 5 static directories (2 in the top-level project directory, 1 in the directory that includes settings.py, and 2 more for each of my 2 web apps - - so 7 total). Some directories are hidden with .gitignore and others are not. In one of my other projects, when I was first starting out, when I wanted to make a change to a CSS file, I made a change to the code, but the effect on the webpage was unapparent. So to experiment, I tried making changes in 5 other places until I finally got it to work. Additionally, if I forgot to clear my CSS cache, that could be part of the problem as well. In my main Django project which is mostly finished and deployed in production, I know which CSS file I need change locally and then when I push to prod, the Heroku Python / Django buildpack runs the collect static command automatically and everything just works. I don’t even have to think about it. But here I am today working on a brand-new project, and the static file configuration is completely different and I am unsure which CSS files I need to make changes to or which static folder is the one which Django needs to refer to to get CKEditor to work.

The official Django tutorial that you linked to Ken recommends this (as a general example for the polls app):

STATICFILES_DIRS = [
    "/home/special.polls.com/polls/static",
    "/home/polls.com/polls/static",
    "/opt/webfiles/common",
]

That’s basic. Everywhere else on the web (Udemy courses, and SO) use dynamic path references using the Python builtin os module. Here are my 5 static file declaration lines in my current settings.py:

STATIC_URL = '/staticfiles/'
STATICFILES_DIRS = [ os.path.join(BASE_DIR,'static') ]
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
CKEDITOR_BASEPATH = "/staticfiles/ckeditor/ckeditor/"

For the CKEditor base path line, it looks like the authors of the app may have ‘taken the liberty’ of using (or assuming?) /staticfiles/ is the active static directory. Maybe they assumed that the end users of the web app are smart enough to understand how to handle Django static files and change it accordingly to meet their unique usecase. Evidently I am not that smart. In my setup as you can see in the above snippet, I build the STATICFILES_DIRS string using the .join() casting technique (as most other people on the web do that the official Django docs don’t address). Yet I also have a STATIC_URL set to '/staticfiles/'. So which is it? My question at this point boils down to this @KenWhitesell or anyone else following along this sordid commentary: Which one is needed and which one should I modify or get rid of?

I already tried changing "/staticfiles/ckeditor/ckeditor/" to "/static/ckeditor/ckeditor/". That didn’t resolve the issue.

I have the following note in my settings.py which I have commented out but kept it there for my future reference:

# I got the below from:
# https://stackoverflow.com/questions/53859972/django-whitenoise-500-server-error-in-non-debug-mode
''' 
STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage" 
'''

The reason why I kept that in as a comment in my settings.py is because I spent considerable time troubleshooting my static files configuration while mashing the keyboard several weeks ago and using that whitenoise element was an actual potential candidate for inclusion because I thought it could be a working solution.

Did you also move the necessary directories and files from the staticfiles directory to your static directory? The configuration change is only half the issue.

Now to address some of your rant -

Yes, I understand the frustration. It also took me a while to understand what each of those settings do, and how they work together to produce the desired results. And personally, that’s what I would encourage you to focus on. Spend the time to understand what each of those settings are directing Django to do. Don’t just look for pat answers on some website without trying to understand why they’re providing what they’re providing.

I’d suggest starting with a review of the docs for STATICFILES_DIRS and STATIC_URL. (Those are the only two you need for a development environment. STATIC_ROOT comes into play when you’re ready to deploy this for production.)

<opinion>
I’ve stated it many times here and in other contexts. I consider SO to be so bad that I consider it dangerous. I will never recommend it in isolation or without external verifications. I never encourage people to seek out information there. It’s probably the worse way to try and learn something. (Aside from the frequently-outdated and simply wrong answers that I have seen - and seen upvoted, my biggest issue with it is that there’s rarely any effort put in to identify the context of the question, or how a slightly different situation would change the answer. And so what I see is people trying to use some answer that they found on SO without understanding why their circumstance is different from the original question being asked and not being able to understand why the answer doesn’t work for them.) As a result, SO has absolutely no credability with me.
<opinion>

I’m sorry, what tutorial did I link to that has that definition for STATICFILES_DIRS? I can’t find that sample anywhere on djangoproject.com.

I made the configuration variable change in settings.py but I didn’t follow up with a new collect static command and I didn’t otherwise move the files over to the new directory reference. But if this is what is required, then that would wreck my source code in a major way because I don’t want to track CKEditor static files in my git repo. I would need to add /static/ to my .gitignore which would, in turn, ignore some of my other files (such as CSS) which I am actively changing to craft the form of some of my web pages. Before I change "/staticfiles/ckeditor/ckeditor/" to "/static/ckeditor/ckeditor/" and run collect static (which would be a total disaster), I will yield your advice by reading the official Django docs on static files so I can get it right. That’s my next step.

If I am going to focus on understanding what settings do and the reasoning behind best practices, you are absolutely right when you suggest reviewing the official Django docs covering STATICFILES_DIRS, STATIC_URL, and STATIC_ROOT.

With regards to your reflections on the usefullness of Stack Overflow, you have made an observation that I also notice in general in my experience: SO content for Django error troubleshooting that appears in top-level search results in abundance are rarely recent. Most results that Google serves are from 2013 or earlier. It’s so horribly out of date that most of the content is practically useless. It’s trash because most of what ever Google returns when searching for Django tracebacks include answers which are no longer relevant. I’m wondering whether this SEO issue is the result of a defect in SO or a defect Google’s algorithm. Regardless of where the defect originates, this is a common irritant of mine in my development workflow. Thank you for pointing out that I am not alone in this crummy user experience, and - - more importantly - - that the best solution is to stick to reading the official docs.

My original explanation earlier where I talked about having so many different static directories scattered across my source code is caused by the exact problem with SO that you pointed out. My mess of static files caused by the patchwork of different variables and commands suggested by 5 different users on SO that I leveraged reinforces the lesson you are suggesting to focus primarily on the official Django docs and to cut SO out of the picture. This is my take away here.

The only remaining thoughts I have to share with focusing on the Django docs is that when I am trying to build my website and I am deep into a coding session, when I receive a mysterious “Attribute Error” that I can’t decipher or understand, I want a quick solution. No one wants to spend two hours re-reading Django’s QuerySet API reference for a solution because that is like venturing to find a needle in a haystack where, even after reading for the two hours, there is no guarantee I will be any closer to resolving my Attribute Error. When Google and SO fails, that’s when I resort to asking my question on these Django forums. This is the same reason why I think many newbies arrive on these forums and other forums elsewhere on the web. We just want a quick fix so we can get on with building our website or getting our script to run as intended. No one wants to get bogged down reading all of Django’s 2,000 pages worth of documentation (.pdf) cover to cover nor churning through no longer relevant and aging SO answers.

I get that there are no easy shortcuts or quick fixes and mastering any trade or achieving competency in any field requires dedication and effort. The task in front of me now is to read Django’s docs on static files in order for me to deploy CKEditor as intended.

The link you shared is in post #10 earlier in this thread. This was the link you shared: STATICFILES_DIRS. Alternate link:

  • https://docs.djangoproject.com/en/4.1/ref/settings/#staticfiles-dirs

Here is a screenshot of the entry in the Django doc at that location:

The Django doc covering settings and specifically static files was sparse. It was short. I read it over twice in about 15 minutes. But I still can’t get my static files organized properly. Based on STATICFILES_DIR it is not clear to me if this is the correct line:

  • STATICFILES_DIRS = [ os.path.join(BASE_DIR,'static') ]

or if we should change that to:

  • STATICFILES_DIRS = [ os.path.join(BASE_DIR,'staticfiles') ].

And I believe changing STATIC_URL = '/staticfiles/' to STATIC_URL = '/static/' and running collect static could be risky because, as discussed previously, I fear overwriting my other static files and losing valuable custom CSS. The Django docs don’t address this nuanced distinction.

As long and detailed as the Django docs are generally speaking, sometimes there still isn’t enough information to reach a solution. It’s been established that Stack Overflow isn’t a viable alternative. So where do we turn to next?

Sometimes the Django docs seem like they are written by elite software developers for other elite developers with some content being too difficult for novices like me to understand. Other times, the Django docs are seemingly incomplete or insufficient in explaining how to use the service (like in this case for setting up static files). I’ve encountered this irritant on other topics in the Django docs - - they are either too hard to understand or they are so sparse I am left wondering what I should try next.

edit: sp + grammar

I find that to be an interesting perspective - we actually find it preferable to track third-party JS/CSS libraries in our repo, because it makes deployment so much easier.

This is not an accurate statement. You could add /static/ckeditor/ to your .gitignore if you wanted to go that route, allowing all your other files to continue to be tracked.

That’s why I try to provide the relevant link to the appropriate page. I’m very aware of how difficult it can be to find something when you’re not sure what you’re looking for.

That’s not a tutorial, that’s the reference doc for that setting - and yes, there is a very significant difference.

In the docs (outside the tutorial), the examples are minimalist examples of one way a feature can be used.

Those examples are not all-inclusive, they are not designed or intended to say that this is the only, or the best, or even recommended way to use that feature - simply that it is a demonstration of a feature being used.

That’s an important point to keep in mind as you’re reading the rest of the docs. The examples are just that - examples. They’ll show the syntax and (in some cases) the expected results for those examples.

What’s also important to remember is that some of the examples will have other code necessary to make the example work - but that other code isn’t really part of the example - it’s just the “setup” necessary for the feature being highlighted.

One final point to keep in mind is that these are Django docs - not Python docs. There is an assumption being made that the person referencing these docs are sufficiently familiar with Python to understand the terminology and wording being used.

For example, in that STATICFILES_DIRS link we have now both referenced, the last line before the example reads:

This should be set to a list of strings that contain full paths to your additional files directory(ies) e.g.:

This implies that the reader know what a Python list and a Python string are.

The docs would be significantly more unwieldy if they tried to include fundamental Python information as well.

Moving on to your recent edit:

… Or both.

Notice how the docs show multiple entries in STATICFILES_DIRS.

(But I still recommend moving the ckeditor directory into static, and checking it into your git repo - it really is going to make deployment a whole lot easier, unless you’re planning to serve those files from an external resource.)

Recommended resources are identified in the docs: Django Community | Django

I wouldn’t use the term “elite”, but yes, I would acknowledge that they do expect the individual have some degree of proficiency with Python.

Thank you Ken for your guidance and support. I seem to overthink my Django and Python research and questions. I am grateful to have someone read my long-winded troubleshooting ordeals and provide helpful feedback when most other people wouldn’t. Thanks.

I am back today with good news. I got it working! This post is to document my solution.

Taking your advice, I changed:

STATIC_URL = '/staticfiles/'
CKEDITOR_BASEPATH = "/staticfiles/ckeditor/ckeditor/"

to:

STATIC_URL = '/static/'
CKEDITOR_BASEPATH = "/static/ckeditor/ckeditor/"

Next I ran the collectstatic. Django asked me if I wanted to over-write my existing static files. I entered ‘yes’ and then it said “0 changes made”. So I manually moved /ckeditor/ckeditor/directory (originally insidestaticfiles) and moved all of it into the new top-level /static` directory, as you suggested. I didn’t add the directory to .gitignore so they will be tracked.

When I refresh my Admin Dashboard, the web app was still not showing the WYSIWYG editor.

When I view-source in my browser for this app in my Admin Dashboard, in the header the static files are looking to these destinations:

<script src="/admin/jsi18n/"></script>
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/ckeditor/ckeditor-init.js" data-ckeditor-basepath="/static/ckeditor/ckeditor/" id="ckeditor-init-script"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<script src="/static/ckeditor/ckeditor/ckeditor.js"></script>
<script src="/static/admin/js/core.js"></script>
<script src="/static/admin/js/admin/RelatedObjectLookups.js"></script>
<script src="/static/admin/js/actions.js"></script>
<script src="/static/admin/js/urlify.js"></script>
<script src="/static/admin/js/prepopulate.js"></script>
<script src="/static/admin/js/vendor/xregexp/xregexp.js"></script>

As you can see, ckeditor resides inside /static project root which is where in fact all the static files are now located (I just moved them there).

The missing puzzle piece was when I realized that I needed to update the extrahead block in change_form.html from:

<script>window.CKEDITOR_BASEPATH = '/staticfiles/ckeditor/ckeditor/';</script>

to:

<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>

And Presto! Have a look at the final product:

For the record, here is my complete working static configuration variables as declared inside settings.py:

STATIC_URL = '/static/'
STATICFILES_DIRS = [ os.path.join(BASE_DIR,'static') ]
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
CKEDITOR_BASEPATH = "/static/ckeditor/ckeditor/"

What is strange is that when I run collectstatic, Django says:

You have requested to collect static files at the destination
location as specified in your settings:

/home//dev/projects/python/2018-and-2020/hypno_juicer/staticfiles

As you can see Django reports that my settings.py file specifies the destination as staticfiles still even though I have changed all references to static. Not sure what is happening there.