"""A plugin that provides Django-specific functionality when a project
looks like it contains Django files.

Copyright (c) 2010-2021, Wingware All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""

import os
import sys
import time
import collections
import wingapi
from proj import attribs
from wingutils import datatype
from wingbase import fileutils
from wingbase import location
from wingbase import textutils
from guiutils import wgtk
from guiutils import formbuilder
from wingbase import spawn

# Scripts can be internationalize with gettext.  Strings to be translated
# are sent to _() as in the code below.
import gettext
_ = gettext.translation('scripts_django', fallback=True).gettext

# This special attribute is used so that the script manager can translate
# also docstrings for the commands found here
_i18n_module = 'scripts_django'

def _appname_formlet():
  apps = _CDjangoPluginActivator._instance._GetDjangoAppDirs()
  appnames = [os.path.basename(a) for a in apps]
  choices = [(a, a) for a in appnames]
  return formbuilder.CPopupChoiceGui(choices)

def _get_actions_list(actions):
  msg = _("The following actions were completed successfully:\n\n")
  for i, action in enumerate(actions):
    msg += "(%i) %s" % (i+1, action)
    if i < len(actions) - 2:
      msg += ",\n"
    elif i < len(actions) - 1:
      msg += _(", and\n")
    else:
      msg += '.'
  return msg
  
def _get_errors_list(errs):
  if not errs:
    return ''
  else:
    msg = _("Some errors occurred.  You may need to correct these manually:")
    for err in errs:
      msg += '\n\n* %s' % err
    return msg + '\n\n'
  
def _get_eol(txt):
  if txt.find('\r\n') >= 0:
    eol = '\r\n'
  elif txt.find('\r') >= 0:
    eol = '\r'
  else:
    eol = '\n'
  return eol

_kMissingPythonMessage = _("You may need to configure Python Executable in Project Properties")
def _get_output(output, separator='\n\n', pfx=('Stdout: ', 'Stderr: '), verbose=0):
  retval = []
  if output[0]:
    retval.append(pfx[0] + output[0].rstrip() + separator)
  if output[1]:
    retval.append(pfx[1] + output[1].rstrip())
  if verbose and output[1]:
    retval.append(separator + _kMissingPythonMessage)
  return ''.join(retval)

gVersionCache = {}
def _get_version_cache_key():
  
  cmdline, dirname, err = _get_base_cmdline()
  if err:
    return None

  cmdline.append('--version')
  
  env = wingapi.gApplication.GetProject().GetEnvironment(set_pypath=False)
  if 'DJANGO_SITENAME' in env:
    del env['DJANGO_SITENAME']
  if 'DJANGO_SETTINGS_MODULE' in env:
    del env['DJANGO_SETTINGS_MODULE']
  env_items = tuple(sorted(env.items()))
  
  target = wingapi.gApplication.GetExecutionTarget()
  
  return (tuple(cmdline), env_items, dirname, target)

def _version_gte(maj, min):

  key = _get_version_cache_key()
  
  # Assume new version if can't be determined or is still being determined async
  version = gVersionCache.get(key, None)
  if version is None:
    return True
  
  return version >= (maj, min)

def _get_base_cmdline():
  manage_py, settings_py = _CDjangoPluginActivator._instance._FindKeyFiles()
  if manage_py is None:
    return None, None, _("Could not find manage.py and/or settings.py in the project")
  project = wingapi.gApplication.GetProject()
  py = project.GetPythonExecutable(None)
  if py is None:
    py = wingapi.gApplication.FindPython()
  if py is None:
    return None, None, _("Could not find Python.  Please set Python Executable in Project Properties, accessed from the Project menu")
  props = project.GetPythonExecutableProperties()
  if props is None or props['fullpath'] is None:
    return None, None, _("Could not find Python executable; inspection in progress")
    
  if manage_py.startswith('ssh://'):
    loc = wingapi._FilenameToLocation(manage_py)
    manage_py = loc._GetNativeFullPath()
    if manage_py is None:
      loc._RunInspect()
      return None, None, _("Could not determine full path for manage.py due to pending remote inspection")
    
  manage_dir = os.path.dirname(manage_py)

  return [props['fullpath'], manage_py], manage_dir, None
  
def django_start_app(appname):
  """Start a new application within the current Django project and add it to the 
  INSTALLED_APPS list in the project's settings.py file."""
  
  actions = []
  errs = []
  
  app = wingapi.gApplication
  basecmdline, dirname, err = _get_base_cmdline()
  if err is not None:
    title = _("Failed to Start App")
    msg = _("The Django app could not be created:  %s") % err
    app.ShowMessageDialog(title, msg)
    return
  cmdline = basecmdline + ['startapp', appname]

  def terminated(timeout, exc, errno):
    
    output = (proc.GetOutput(), proc.GetStdErrOutput())
    if timeout or exc or errno != 0 or output[1]:
      if timeout:
        reason = _("Sub-process timed out")
      elif exc:
        reason = _("Failed to start sub-process. Error: {}").format(exc)
      elif output[1] and errno == 0:
        reason = _("The command %s succeeded but printed unexpected output") % str(cmdline)
      else:
        reason = _("Migration failed with exit code {}").format(errno)
      title = _("Failed to Start New App")
      msg = _("Could not start new app: %s") % reason
      out = _get_output(output)
      if out:
        msg += '\n\n' + out
      app.ShowMessageDialog(title, msg)
      return
    
    actions.append(_("Created Django app %s in %s") % (appname, dirname))
    _add_app_to_settings(appname, actions, errs)
    
  env = app.GetProject().GetEnvironment()
  proc = app.CreateChildProcess(cmdline, terminated, env=env, dirname=dirname,
                                separate_stderr=False, timeout=5.0)
 
def _add_app_to_settings(appname, actions, errs):
  """Add the given new appname to the settings, if possible, and then display a
  confirmation"""
    
  app = wingapi.gApplication
  
  def show_confirmation():    
    title = _("The App was Created")
    msg = _("The application was created.  ")
    if errs:
      msg += _get_errors_list(errs)
    msg += _get_actions_list(actions)
    app.ShowMessageDialog(title, msg, modal=False)    
    
  # Add the new app to INSTALLED_APPS in settings.py
  manage_py, settings_py = _CDjangoPluginActivator._instance._FindKeyFiles()
  if wingapi._FilenameToLocation(settings_py).IsDirectory():
    errs.append(_("Could not edit Django settings -- App %s needs to be added manually to INSTALLED_APPS") % appname)
    show_confirmation()
    return
  
  def read_complete(err, txt, encoding):
    if err:
      errs.append(_("Unable to read {} to update INSTALLED_APPS").format(settings_py))
      show_confirmation()
      return
    
    use_name = appname
    if _version_gte(1, 10):
      use_name += '.apps.' + appname.capitalize() + 'Config'
    lines = txt.splitlines()
    eol = _get_eol(txt)
    insert_line = None
    in_installed_apps = False
    for i, line in enumerate(lines):
      if line.lstrip().startswith('INSTALLED_APPS'):
        if line.strip().endswith('['):
          in_installed_apps = ']'
        else:
          in_installed_apps = ')'
      elif in_installed_apps and line.strip().startswith(in_installed_apps):
        in_installed_apps = False
        insert_line = i
        break
    if insert_line is None:
      lines.extend(['', 'INSTALLED_APPS = [', "    '%s'," % use_name, ']', ''])
    else:
      if lines[insert_line-1].rstrip().endswith(',') or lines[insert_line-1].lstrip().startswith('INSTALLED_APPS'):
        lines = lines[:insert_line] + ["    '%s'," % use_name] + lines[insert_line:]
      else:
        lines = lines[:insert_line-1] + \
          [lines[insert_line-1].rstrip() + ',', "    '%s'," % use_name] + \
          lines[insert_line:]
        
    def write_complete(err):
      if err:
        errs.append(_("Unable to write {} to update INSTALLED_APPS").format(settings_py))
      else:
        actions.append(_("Added %s to INSTALLED_APPS in %s") % (use_name, settings_py))
      show_confirmation()
      
    app.WriteFile(settings_py, eol.join(lines), write_complete, encoding)
    
  app.ReadFile(settings_py, read_complete)
  
django_start_app.contexts = [wingapi.kContextNewMenu(_("Djang_o"), group=2)]
django_start_app.label = _("Start New Django Application")
django_start_app.flags = { 'force_dialog_argentry': True }

def django_migrate_db():
  """Run manage.py migrate (or syncdb in Django 1.7 and earlier)"""
  
  app = wingapi.gApplication
  cmdline, dirname, err = _get_base_cmdline()
  if err is not None:
    title = _("Failed to Migrate DB")
    msg = _("Could not migrate db:  %s") % err
    app.ShowMessageDialog(title, msg)
    return
  
  if _version_gte(1, 8):
    cmd = 'migrate'
  else:
    cmd = 'syncdb'
  
  cmdline += [cmd, '--noinput']
  
  def terminated(timeout, exc, errno):
    if timeout:
      msg = _("Could not migrate the database: Sub-process timed out")
    elif exc:
      msg = _("Could not migrate the database: Sub-process failed to start: %s") % str(exc)
    elif errno != 0:
      msg = _("Could not migrate the database: Sub-process failed with errno=%s") % str(errno)
    else:
      msg = _("Migrate DB completed")
        
    stdout = proc.GetOutput()
    stderr = proc.GetStdErrOutput()
    
    if stderr:
      msg += '\n\n' + stderr + '\n\n' + _kMissingPythonMessage
    if stdout:
      msg += '\n\n' + stdout
      
    if not terminated and not exc and errno == 0 and not stderr and not stdout:
      title = msg
      msg = _("Migrate DB completed with no errors or output")
      app.ShowMessageDialog(title, msg)
      
    else:
      title = _("Django Migrate DB")
      editor = app.ScratchEditor(title, 'text/plain')
      doc = editor.GetDocument()
      doc.SetText(msg)
  
  env = app.GetProject().GetEnvironment()
  proc = app.CreateChildProcess(cmdline, terminated, env=env, dirname=dirname, timeout=100)
  
django_migrate_db.contexts = [wingapi.kContextNewMenu(_("Djang_o"), group=1)]
django_migrate_db.label = _("Migrate Database")

def django_run_tests():
  """Run manage.py unit tests in the Testing tool"""
  
  wingapi.gApplication.ShowTool('testing')
  
  manage_py, settings_py = _CDjangoPluginActivator._instance._FindKeyFiles()
  cmd = 'run_test_files(locs="%s")' % manage_py
  wingapi.gApplication.ExecuteCommand(cmd)
  
django_run_tests.contexts = [wingapi.kContextNewMenu(_("Djang_o"), group=1)]
django_run_tests.label = _("Run Unit Tests")

def django_run_tests_to_scratch_buffer():
  """Run manage.py tests with output in a scratch buffer"""
  
  app = wingapi.gApplication
  
  cmdline, dirname, err = _get_base_cmdline()
  if err is not None:
    title = _("Failed to run Django unit tests")
    msg = _("Could not run Django :  %s") % err
    app.ShowMessageDialog(title, msg)
    return
  
  cmdline += ['test']
  
  editor = app.ScratchEditor(_("Django Unit Tests"), 'text/plain')
  doc = editor.GetDocument()
  start = time.time()
  doc.SetText(_('Starting Django Unit Tests %s:\n') % time.ctime())

  def output_received(stdout, stderr):
    output = proc.GetOutput(remove=True)
    editor = app.ScratchEditor(_("Django Unit Tests"), 'text/plain')
    doc = editor.GetDocument()
    doc.InsertChars(doc.GetLength(), output)    
  
  def terminated(timeout, exc, errno):
    if timeout or exc:
      if timeout:
        msg = _("Could not run Django unit tests: Sub-process timed out")
      elif err is not None:
        msg = _("Could not run Django unit tests: Sub-process failed to start: %s") % str(exc)
    else:
      msg = _("Django unit tests completed after %s seconds with errno=%s") % (str(time.time() - start), str(errno))
      
    editor = app.ScratchEditor(_("Django Unit Tests"), 'text/plain')
    doc = editor.GetDocument()
    doc.InsertChars(doc.GetLength(), '\n' + msg)
  
  env = app.GetProject().GetEnvironment()
  proc = app.CreateChildProcess(cmdline, terminated, output_received, env=env, dirname=dirname,
                                timeout=120, separate_stderr=False)

def django_migrate_app(appname):
  """Run manage.py makemigrations for given app name and display the output in a
  scratch buffer."""
  
  app = wingapi.gApplication
  cmdline, dirname, err = _get_base_cmdline()
  if err is not None:
    title = _("Failed to Make Migrations")
    msg = _("Could not migrate app: %s") % err
    app.ShowMessageDialog(title, msg)
    return
  
  cmdline += ['makemigrations', appname]
  
  def terminated(timeout, exc, errno):
    
    if timeout or exc or errno != 0:
      if timeout:
        reason = _("Sub-process timed out")
      elif exc:
        reason = _("Failed to start sub-process. Error: {}").format(exc)
      else:
        reason = _("Migration failed with exit code {}").format(errno)
      title = _("Failed to Make Migrations")
      msg = _("Could not migrate app: %s") % reason
      output = (proc.GetOutput(), proc.GetStdErrOutput())
      out = _get_output(output)
      if out:
        msg += '\n\n' + out
      app.ShowMessageDialog(title, msg)

    else:
      editor = app.ScratchEditor(_("Django App Migration"), 'text/plain')
      doc = editor.GetDocument()
      output = (proc.GetOutput(), proc.GetStdErrOutput())
      doc.SetText(_get_output(output, pfx=('', ''), verbose=1))
  
  env = app.GetProject().GetEnvironment()
  proc = app.CreateChildProcess(cmdline, terminated, env=env, dirname=dirname, separate_stderr=False)

django_migrate_app.contexts = [wingapi.kContextNewMenu(_("Djang_o"), group=1)]
django_migrate_app.label = _("Migrate App")
django_migrate_app.arginfo = {
  'appname': wingapi.CArgInfo(_("Django App Name"), datatype.CType(''),
                              _appname_formlet)
}
django_migrate_app.flags = { 'force_dialog_argentry': True }

def _django_migrate_app_avail():
  if not _version_gte(1, 8):
    return False
  apps = _CDjangoPluginActivator._instance._GetDjangoAppDirs()
  return len(apps) > 0
django_migrate_app.available = _django_migrate_app_avail

def django_show_migrations():
  """Run manage.py showmigrations and display the output in a scratch buffer."""
  
  app = wingapi.gApplication
  
  cmdline, dirname, err = _get_base_cmdline()
  if err is not None:
    title = _("Failed to Show Migrations")
    msg = _("Could not show migrations: %s") % err
    app.ShowMessageDialog(title, msg)
    return
  
  cmdline += ['showmigrations']
  
  def terminated(timeout, exc, errno):
    
    output = (proc.GetOutput(), proc.GetStdErrOutput())
    
    if timeout or exc:
      if exc:
        reason = _("Failed to start sub-process: %s") % timeout
      else:
        reason = _("Sub-process timed out")
      title = _("Failed to Show Migrations")
      msg = _("Could not show migrations: %s") % reason
      out = _get_output(output)
      if out:
        msg += '\n\n' + out
      app.ShowMessageDialog(title, msg)
      return
    
    editor = app.ScratchEditor(_("Django Show Migrations"), 'text/plain')
    doc = editor.GetDocument()
    doc.SetText(_get_output(output, pfx=('', ''), verbose=1))
    
  env = app.GetProject().GetEnvironment()
  proc = app.CreateChildProcess(cmdline, terminated, env=env, dirname=dirname, timeout=5.0)

django_show_migrations.contexts = [wingapi.kContextNewMenu(_("Djang_o"), group=1)]
django_show_migrations.label = _("Show Migrations")

def _django_show_migrations_avail():
  return _version_gte(1, 8)
django_show_migrations.available = _django_show_migrations_avail

def django_collect_static():
  """Run' manage.py collectstatic --noinput' in the OS Commands tool."""
  
  from edit import cap_oscommands
  from proj import createproject
  
  app = wingapi.gApplication
  
  def failed(reason):
    title = _("Failed to Collect Static Files")
    msg = _("Could not collectstatic: {}.".format(reason))
    app.ShowMessageDialog(title, msg)
    
  name = createproject._CSetup_django.kCollectStaticCommandName
  view = cap_oscommands.GetOSCommandsPanel()
  if view is None:
    failed(reason=_("Missing OS Commands Tool"))
    return
    
  cmd = view._FindCmdForEntryPoint(name)
  if cmd is None:
    manage_py, settings_py = _CDjangoPluginActivator._instance._FindKeyFiles()
    createproject._CSetup_django._ClsCreateCollectStaticOSCommand(app.fSingletons, wingapi._FilenameToLocation(manage_py))
    print("Created missing OS Command for collectstatic")
    cmd = view._FindCmdForEntryPoint(name)
    
  if cmd is None:
    failed(_("Could not find OS Command"))
    return
    
  try:
    app.ExecuteOSCommand(cmd.id)
  except:
    exc_type, exc_value, traceback = sys.exc_info()
    failed("Could not execute OS Command: {}".format(exc_value))
    traceback.print_exception()

django_collect_static.contexts = [wingapi.kContextNewMenu(_("Djang_o"), group=1)]
django_collect_static.label = _("Collect Static Files")

def django_validate():
  """Run manage.py check (or validate in Django 1.5 and earlier)"""
  
  app = wingapi.gApplication
  cmdline, dirname, err = _get_base_cmdline()
  if err is not None:
    title = _("Failed to Validate")
    msg = _("Could not validate:  %s") % err
    app.ShowMessageDialog(title, msg)
    return
  
  if _version_gte(1, 6):
    cmdline += ['check']
  else:
    cmdline += ['validate']
    
  def terminated(timeout, exc, errno):

    output = (proc.GetOutput(), proc.GetStdErrOutput())
    
    if timeout or exc:
      if timeout:
        reason = _("Sub-process timed out")
      else:
        reason = _("Sub-process failed to start: %s") % exc
      title = _("Could Not Validate")
      msg = _("Could not validate: %s") % reason
      out = _get_output(output)
      if out:
        msg += '\n\n' + out
      app.ShowMessageDialog(title, msg)
      return

    if output[0].find('0 errors') == 0 or 'identified no issues' in output[0]:
      app.ShowMessageDialog(_("Validate Succeeded"), _("Validated with 0 errors found"))
      
    else:
      editor = app.ScratchEditor(_("Django Validate"))
      doc = editor.GetDocument()
      doc.SetText(_get_output(output, pfx=('', ''), verbose=1))
  
  env = app.GetProject().GetEnvironment()
  proc = app.CreateChildProcess(cmdline, terminated, env=env, dirname=dirname, timeout=5.0)
    
django_validate.contexts = [wingapi.kContextNewMenu(_("Djang_o"), group=1)]
django_validate.label = _("Run Check")

def django_show_docs():
  """Show documentation for using Wing and Django together"""
  app = wingapi.gApplication
  app.ExecuteCommand('show-document', section="howtos/django")
  
django_show_docs.contexts = [wingapi.kContextNewMenu(_("Djang_o"), group=3)]
django_show_docs.label = _("Show Documentation")

def django_restart_shell():
  """Show and restart the Python Shell tool, which works in the same
  environment as "manage.py shell".  To show the tool without restarting
  it, use the Tools menu."""
  
  singles = wingapi.gApplication.fSingletons
  shell = singles.fGuiMgr.ShowPanel('python-shell', flash=True, grab_focus=True)
  if shell is not None:
    shell.fOwner.ScheduleRestart()
  
django_restart_shell.contexts = [wingapi.kContextNewMenu(_("Djang_o"), group=1)]
django_restart_shell.label = _("Restart Shell")


#########################################################################
# Support code and setup auto-activation of this script as a plugin
# for Django projects

class _CDjangoPluginActivator:
  """Tracks whether the Django plugin should be enabled or not. Install
  signals so plugin can activate or deactivate based on what project is open
  and what it contains."""

  _instance = None
  if 0:
    isinstance(_instance, _CDjangoPluginActivator)
  
  def __init__(self, plugin_id):

    assert _CDjangoPluginActivator._instance is None
    _CDjangoPluginActivator._instance = self
    
    self.plugin_id = plugin_id
    self._project_connections = []
    self.__fCachedFiles = None
    self.__fInspectSequence = 0
    self.__fInspectCurrent = None
    self.__fAppNames = []
    self.__fUpdateTimeout = None
    
    app = wingapi.gApplication
    app.Connect('project-open', self.__CB_ProjectOpen)
    app.Connect('python-runtime-changed', self.ScheduleUpdate)
    self.ConnectToProject()
        
  def ConnectToProject(self):
    """Connect to signals on project so plugin can enable when appropriate"""
    
    # Remove old connections
    for proj, signal in self._project_connections:
      if not proj.destroyed():
        proj.Disconnect(signal)
    self._project_connections = []
      
    # Connect to new project
    app = wingapi.gApplication
    proj = app.GetProject()
    self._project_connections.append((proj, proj.Connect('files-added', self.__CB_ProjectChanged, proj)))
    self._project_connections.append((proj, proj.Connect('files-removed', self.__CB_ProjectChanged, proj)))
    self.ScheduleUpdate()
    
  def __CB_ProjectOpen(self, *args):
    """Current project has changed"""
    
    self.ConnectToProject()

  def __CB_ProjectChanged(self, files):
    """Project contents have changed (given files were added or removed)"""
    
    if self.__fUpdateTimeout is None:
      self.ScheduleUpdate()
      
  def ScheduleUpdate(self):
    """Avoid constant scanning of project during file discovery; we scan
    three seconds after the last project file update"""
    
          
    if self.__fUpdateTimeout is not None:
      wingapi.gApplication.RemoveTimeout(self.__fUpdateTimeout)
    
    self.__fUpdateTimeout = wingapi.gApplication.InstallTimeout(3000, self._DoUpdate)
    
  def _DoUpdate(self):

    self.__fUpdateTimeout = None
    self.__fCachedFiles = None
    self.__fAppNames = []
    self._FindKeyFiles(force=True)

    proj = wingapi.gApplication.GetProject()
    if proj is not None:
      py = proj.GetPythonExecutable(None)
      if py is not None:
        proj.GetPythonExecutableProperties()
  
  def _FindSettingsFile(self, settings, pypath):
    """Look up the full path to the settings.py file for the given DJANGO_SETTINGS_MODULE
    and environment"""
    
    settings = settings.replace('.', os.sep)
    for dirname in pypath.split(os.pathsep):
      fp = os.path.join(dirname, settings)
      if os.path.isdir(fp) and os.path.isfile(os.path.join(fp, '__init__.py')):
        return fp
      elif os.path.isfile(fp + '.py'):
        return fp + '.py'
      
    return None
  
  def _FindKeyFiles(self, force=False):
    """Try to find key Django project files in the project.  Returns full path
    to manage.py and settings.py (or settings package) or (None, None) if not found.
    This runs asynchronously and returns (None, None) while the search is still
    running.  It enables or disables the plugin once the inspection is complete,
    depending on whether it looks like a Django project."""
    
    from proj import createproject
    
    app = wingapi.gApplication

    def _inspect_complete(managepy=None, settingspy=None, error=None):
      
      try:

        if error is not None:
          self.__fCachedFiles = (None, None)
          print(error)
        else:
          self.__fCachedFiles = (managepy, settingspy)
          
        app.EnablePlugin(self.plugin_id, self._IsDjangoProject())
        self._FindDjangoVersion()
        self._FindDjangoAppDirs()
          
      finally:
        self.__fInspectCurrent = None
      
    def inspect_complete(managepy=None, settingspy=None, error=None):
      
      # Late call from earlier attempt before project changed again
      if self.__fInspectCurrent != self.__fInspectSequence:
        return
      
      _inspect_complete(wingapi._LocationToFilename(managepy), wingapi._LocationToFilename(settingspy), error)
      
    def prompt_user(pairs):
  
      if len(pairs) > 1:
        app.SetStatusMessage("Warning: Multiple manage.py/settings.py pairs found in project")
    
      ignore, managepy, settingspy = pairs[0]
      inspect_complete(managepy, settingspy)
    
    # No project is open right now
    proj = app.GetProject()
    if not proj:
      return (None, None)

    # Prefer value selected in existing project configuration, as set with DJANGO_SETTINGS_MODULE
    # and the main entry point
    env = proj.GetEnvironment()
    if 'DJANGO_SETTINGS_MODULE' in env:
      settingspy = self._FindSettingsFile(env['DJANGO_SETTINGS_MODULE'], env.get('PYTHONPATH', ''))
    else:
      settingspy = None
    managepy = proj.GetMainEntryPoint()
    if managepy is not None and managepy.startswith('entry:'):
      try:
        managepy = proj.GetNamedEntryPointAttrib(managepy[len('entry'):], 'filename')
      except KeyError:
        managepy = None
    if managepy is not None and not managepy.endswith('manage.py'):
      managepy = None
    if managepy is not None and settingspy is not None:
      if self.__fCachedFiles is None:
        _inspect_complete(managepy, settingspy)
      return (managepy, settingspy)

    # Use cache if we've already inspected
    if not force and self.__fCachedFiles is not None:
      return self.__fCachedFiles

    # Inspection is still in progress so don't have values yet
    if not force and self.__fInspectCurrent is not None:
      return (None, None)
    
    # Inspect all the possibly relevant locations asynchronously
    self.__fInspectSequence += 1
    self.__fInspectCurrent = self.__fInspectSequence
    createproject._CSetup_django._FindKeyFilesImpl([], inspect_complete, prompt_user)
    
    return (None, None)  
  
  def _FindDjangoVersion(self):
    """Asynchronously find the version of Django being used so it can later be
    checked in _version_gte()"""
    
    key = _get_version_cache_key()
    if key is None:
      return
    
    if key in gVersionCache:
      return

    def terminated(timeout, exc, exit_code):
      if timeout or exc or exit_code != 0 or proc.GetStdErrOutput():
        ver = None
      else:
        ver = proc.GetOutput().strip()
        ver = ver.split('.')
        if len(ver) > 2:
          ver = ver[:2]
        if len(ver) != 2:
          ver = None
        else:
          try:
            ver = (int(ver[0]), int(ver[1]))
          except:
            ver = None
            
      gVersionCache[key] = ver
    
    cmdline = list(key[0])
    proc = wingapi.gApplication.CreateChildProcess(cmdline, terminated, env=dict(key[1]),
                                                   dirname=key[2])
    
  def _FindDjangoAppDirs(self):
    """Find a list of the app directories in the current project. Returns a
    list of the full path to the app directory. The app name is
    os.path.basename() of each path."""
    
    def join_path(a, b, c):
      if a.startswith('ssh://'):
        return a + '/' + b + '/' + c
      else:
        return os.path.join(a, b, c)
      
    manage_file, settings_file = self._FindKeyFiles()
    if manage_file is None:
      return
    
    def inspect_complete(exc, result):
      if exc:
        return
      
      candidates = []
      for path, info in result.items():
        for dn in info.dirs:
          candidates.extend([
            join_path(path, dn, 'models.py'),
            join_path(path, dn, 'views.py')
          ])
        break
      
      def inspected_candidates(exc, result):
        if exc:
          return
        valid = collections.defaultdict(int)
        for fn, info in result.items():
          if info.modtime:
            valid[os.path.dirname(fn)] += 1
            
        appnames = []
        for dn in valid:
          if valid[dn] == 2:
            appnames.append(dn)
            
        self.__fAppNames = appnames
      
      wingapi.gApplication.InspectFiles(candidates, inspected_candidates, force=True)
      
    wingapi.gApplication.InspectFiles([os.path.dirname(manage_file)], inspect_complete, force=True)
  
  def _GetDjangoAppDirs(self):
    """Get a list of the app directories in the current project. Returns a
    list of the full path to the app directory."""
    
    return self.__fAppNames
  
  def _IsDjangoProject(self):
    """Try to detect if this is a Django project, based on its contents"""
    
    manage_file, settings_file = self._FindKeyFiles()
    if manage_file is None or settings_file is None:
      return False
    
    return True
  
# This sets this script up as a plugin and sets up determination of when the
# plugin is enabled. Note that users can disable plugins globally or at the
# project level to override the return value of this call.
def _django_activator(plugin_id):
  """Plugin activator for Django.  Note this must call CAPIApplication.EnablePlugin()
  and return True to enable or False to disable initially"""
  if _CDjangoPluginActivator._instance is None:
    _CDjangoPluginActivator(plugin_id)
  inst = _CDjangoPluginActivator._instance
  is_django = inst._IsDjangoProject()
  wingapi.gApplication.EnablePlugin(plugin_id, is_django)
  return is_django

_plugin = (_("Django Extensions"), _django_activator)


