Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

2559 lines
99 KiB

;;; python-django.el --- A Jazzy package for managing Django projects
;; Copyright (C) 2011 Free Software Foundation, Inc.
;; Author: Fabián E. Gallina <fabian@anue.biz>
;; URL: https://github.com/fgallina/python-django.el
;; Package-Version: 20150822.404
;; Package-Commit: fc54ad74f0309670359b939f64d0f1fff68aeac4
;; Version: 0.1
;; Maintainer: FSF
;; Created: Jul 2011
;; Keywords: languages
;; This file is NOT part of GNU Emacs.
;; python-django.el is free software: you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation, either version 3 of the
;; License, or (at your option) any later version.
;; python-django.el is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with python-django.el. If not, see
;; <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Django project management package with the goodies you would expect
;; and then some. The project buffer workings is pretty much inspired
;; by the good ol' `magit-status' buffer.
;; This package relies heavily in fgallina's `python.el' available in
;; stock Emacs>=24.3 (or https://github.com/fgallina/python.el).
;; Implements File navigation (per app, STATIC_ROOT, MEDIA_ROOT and
;; TEMPLATE_DIRS), Etag building, Grep in project, Quick jump (to
;; settings, project root, virtualenv and docs), Management commands
;; and Quick management commands.
;; File navigation: After opening a project, a directory tree for each
;; installed app, the STATIC_ROOT, the MEDIA_ROOT and each
;; TEMPLATE_DIRS is created. Several commands are provided to work
;; with the current directory at point.
;; Etags building: Provides a simple wrapper to create etags for
;; current opened project.
;; Grep in project: Provides a simple way to grep relevant project
;; directories using `rgrep'. You can override the use of `rgrep' by
;; tweaking the `python-django-cmd-grep-function'.
;; Quick jump: fast key bindings to jump to the settings module, the
;; project root, the current virtualenv and Django official web docs
;; are provided.
;; Management commands: You can run any management command from the
;; project buffer via `python-django-mgmt-run-command' or via the
;; quick management commands accesible from the Django menu.
;; Completion is provided for all arguments and you can cycle through
;; opened management command process buffers very easily. Another
;; cool feature is that comint processes are spiced up with special
;; processing, for instance if are using runserver and get a
;; breakpoint via pdb or ipdb the pdb-tracking provided by
;; `python-mode' will trigger or if you enter dbshell the proper
;; `sql-mode' will be used.
;; Quick management commands: This mode provides quick management
;; commands (management commands with sane defaults, smart prompt
;; completion and process extra processing) defined to work with the
;; most used Django built-in management commands like syncdb, shell,
;; runserver, test; several good ones from `django-extensions' like
;; shell_plus, clean_pyc; and `south' ones like convert_to_south,
;; migrate, schemamigration. You can define new quick commands via
;; the `python-django-qmgmt-define' and define ways to handle when
;; it's finished by defining a callback function.
;;; Usage:
;; The main entry point is the `python-django-open-project'
;; interactive function, see its documentation for more info on its
;; behavior. Mainly this function requires two things, a project path
;; and a settings module. How you chose them really depends on your
;; project's directory layout. The recommended way to chose your
;; project root, is to use the directory containing your settings
;; module; for instance if your settings module is in
;; /path/django/settings.py, use /path/django/ as your project path
;; and django.settings as your settings module. Remember to always
;; set `python-shell-interpreter' to either python or python2 and
;; never use iPython directly as Django enables it automatically when
;; the shell is started.
;;; Installation:
;; Add this to your .emacs:
;; (add-to-list 'load-path "/folder/containing/file")
;; (require 'python-django)
;;; Code:
(require 'hippie-exp)
(require 'json)
(require 'python)
(require 'sql)
(require 'tree-widget)
(require 'wid-edit)
(require 'widget)
;; Avoid compiler warnings
(defvar view-return-to-alist)
(defgroup python-django nil
"Python Django project goodies."
:group 'convenience
:version "24.2")
;;; keymaps
(defvar python-django-mode-map
(let ((map (make-keymap)))
(suppress-keymap map t)
(define-key map [remap next-line] 'python-django-ui-widget-forward)
(define-key map [remap previous-line] 'python-django-ui-widget-backward)
(define-key map [remap forward-char] 'widget-forward)
(define-key map [remap backward-char] 'widget-backward)
(define-key map [remap beginning-of-buffer]
'python-django-ui-beginning-of-widgets)
(define-key map [remap newline] 'python-django-ui-safe-button-press)
(define-key map (kbd "^") 'python-django-ui-move-up-tree)
(define-key map (kbd "p") 'python-django-ui-widget-backward)
(define-key map (kbd "n") 'python-django-ui-widget-forward)
(define-key map (kbd "b") 'widget-backward)
(define-key map (kbd "f") 'widget-forward)
(define-key map (kbd "d") 'python-django-cmd-dired-at-point)
(define-key map (kbd "w") 'python-django-cmd-directory-at-point)
(define-key map (kbd "ja") 'python-django-cmd-jump-to-app)
(define-key map (kbd "jm") 'python-django-cmd-jump-to-media)
(define-key map (kbd "jt") 'python-django-cmd-jump-to-template-dir)
(define-key map (kbd "vs") 'python-django-cmd-visit-settings)
(define-key map (kbd "vr") 'python-django-cmd-visit-project-root)
(define-key map (kbd "vv") 'python-django-cmd-visit-virtualenv)
(define-key map (kbd "t") 'python-django-cmd-build-etags)
(define-key map (kbd "s") 'python-django-cmd-grep)
(define-key map (kbd "o") 'python-django-cmd-open-docs)
(define-key map (kbd "h") 'python-django-help)
(define-key map (kbd "m") 'python-django-mgmt-run-command)
(define-key map (kbd "g") 'python-django-refresh-project)
(define-key map (kbd "q") 'python-django-close-project)
(define-key map (kbd "k") 'python-django-mgmt-kill)
(define-key map (kbd "K") 'python-django-mgmt-kill-all)
(define-key map (kbd "u") 'universal-argument)
(define-key map (kbd "$") 'python-django-mgmt-cycle-buffers-forward)
(define-key map (kbd "#") 'python-django-mgmt-cycle-buffers-backward)
(easy-menu-define python-django-menu map "Python Django Mode menu"
`("Django"
:help "Django project tools"
["Run management command"
python-django-mgmt-run-command
:help "Run management command in current project"]
["Kill all running commands"
python-django-mgmt-kill-all
:help "Kill all running commands for current project"]
["Get command help" python-django-help
:help "Get help for any project's management commands"]
["Cycle to next running management command"
python-django-mgmt-cycle-buffers-forward
:help "Cycle to next running management command"]
["Cycle to previous running management command"
python-django-mgmt-cycle-buffers-backward
:help "Cycle to previous running management command"]
"--"
;; Reserved for quick management commands
"---"
["Browse Django documentation"
python-django-cmd-open-docs
:help "Open a Browser with Django's documentation"]
["Build Tags"
python-django-cmd-build-etags
:help "Build TAGS file for python source in project"]
["Dired at point"
python-django-cmd-dired-at-point
:help "Open dired at current tree node"]
["Grep in project directories"
python-django-cmd-grep
:help "Grep in project directories"]
["Refresh project"
python-django-refresh-project
:help "Refresh project"]
"--"
["Visit settings file"
python-django-cmd-visit-settings
:help "Visit settings file"]
["Visit virtualenv directory"
python-django-cmd-visit-virtualenv
:help "Visit virtualenv directory"]
["Visit project root directory"
python-django-cmd-visit-project-root
:help "Visit project root directory"]
"--"
["Jump to app's directory"
python-django-cmd-jump-to-app
:help "Jump to app's directory"]
["Jump to a media directory"
python-django-cmd-jump-to-media
:help "Jump to a media directory"]
["Jump to a template directory"
python-django-cmd-jump-to-template-dir
:help "Jump to a template directory"]))
map)
"Keymap for `python-django-mode'.")
;;; Main vars
(defvar python-django-project-root nil
"Django project root directory.")
(defvar python-django-project-manage.py nil
"Django project manage.py path.")
(defvar python-django-project-settings nil
"Django project settings module.")
(defvar python-django-project-name nil
"Django project name.")
(define-obsolete-variable-alias
'python-django-settings-module
'python-django-project-settings
"24.2")
(define-obsolete-variable-alias
'python-django-info-project-name
'python-django-project-name
"24.2")
;;; Faces
(defgroup python-django-faces nil
"Customize the appearance of Django buffers."
:prefix "python-django-"
:group 'faces
:group 'python-django)
(defface python-django-face-header
'((t :inherit font-lock-function-name-face))
"Face for generic header lines.
Many Django faces inherit from this one by default."
:group 'python-django-faces)
(defface python-django-face-path
'((t :inherit font-lock-type-face))
"Face for paths."
:group 'python-django-faces)
(defface python-django-face-title
'((t :inherit font-lock-keyword-face))
"Face for titles."
:group 'python-django-faces)
(defface python-django-face-django-version
'((t :inherit python-django-face-header))
"Face for project's Django version."
:group 'python-django-faces)
(defface python-django-face-project-root
'((t :inherit python-django-face-path))
"Face for project path."
:group 'python-django-faces)
(defface python-django-face-settings-module
'((t :inherit python-django-face-header))
"Face for project settings module."
:group 'python-django-faces)
(defface python-django-face-virtualenv-path
'((t :inherit python-django-face-header))
"Face for project settings module."
:group 'python-django-faces)
;;; Dev tools
(font-lock-add-keywords
'emacs-lisp-mode
`(("(\\(python-django-qmgmt-define\\)\\>[ \t]\\([^ \t]+\\)"
(1 'font-lock-keyword-face)
(2 'font-lock-function-name-face))))
;;; Error logging
(defvar python-django-error-log-formatter
#'python-django-error-default-formatter)
(defun python-django-error-default-formatter (error-string)
"Formats ERROR-STRING to be placed in the error log."
(format
(concat
"An error occurred retrieving project information.\n"
"Check your project settings and try again:\n\n"
"Current values:\n"
" + python-django-project-root: %s\n"
" + python-django-project-settings: %s\n"
" + python-shell-interpreter: %s\n"
" - found in %s\n\n"
"Details: \n\n%s\n")
python-django-project-root
python-django-project-settings
python-shell-interpreter
(let* ((process-environment
(python-django-info-calculate-process-environment))
(exec-path (python-shell-calculate-exec-path)))
(executable-find python-shell-interpreter))
error-string))
(defun python-django-error-log (error-string)
"Log ERROR-STRING by calling `user-error'."
(user-error "%s" (funcall python-django-error-log-formatter error-string)))
;;; Utility functions
(defun python-django-util-clone-local-variables ()
"Clone local variables from manage.py file.
This function is intended to be used so the project buffer gets
the same variables of python files."
(let* ((file-name
(expand-file-name
python-django-project-manage.py))
(manage.py-exists (get-file-buffer file-name))
(flymake-start-syntax-check-on-find-file nil)
(manage.py-buffer
(or manage.py-exists
(prog1
(find-file-noselect file-name t)
(message nil)))))
;; TODO: Add a predicate parameter to
;; `python-util-clone-local-variables' itself to handle vars not
;; intended to be changed by the variable cloning and replace the
;; following code with that.
(mapc
(lambda (pair)
(and (symbolp (car pair))
(string-match "^python-" (symbol-name (car pair)))
(not (memq (car pair)
'(python-django-project-root
python-django-project-settings
python-django-project-name
python-django-project-manage.py)))
(set (make-local-variable (car pair))
(cdr pair))))
(buffer-local-variables manage.py-buffer))
(when (not manage.py-exists)
(kill-buffer manage.py-buffer))))
(defmacro python-django-util-alist-add (key value alist)
"Update for KEY the VALUE in ALIST."
`(let* ((k (if (bufferp ,key)
(buffer-name ,key)
,key))
(v (if (bufferp ,value)
(buffer-name ,value)
,value))
(elt (assoc k ,alist)))
(if (not elt)
(setq ,alist (cons (list k v) ,alist))
(and (not (member v (cdr elt)))
(setf (cdr elt)
(cons v (cdr elt)))))))
(defmacro python-django-util-alist-del (key value alist)
"Remove for KEY the VALUE in ALIST."
`(let* ((k (if (bufferp ,key)
(buffer-name ,key)
,key))
(v (if (bufferp ,value)
(buffer-name ,value)
,value))
(elt (assoc k ,alist)))
(and elt (setf (cdr elt) (remove v (cdr elt))))))
(defmacro python-django-util-alist-del-key (key alist)
"Empty KEY in ALIST."
`(let* ((k (if (bufferp ,key)
(buffer-name ,key)
,key))
(elt (assoc k ,alist)))
(and elt (setf (cdr elt) nil))))
(defun python-django-util-alist-get (key alist)
"Get values for KEY in ALIST."
(and (bufferp key) (setq key (buffer-name key)))
(cdr (assoc key alist)))
;; Based on `file-name-extension'
(defun python-django-util-file-name-extension (filename)
"Return FILENAME's final \"extension\" sans dot."
(save-match-data
(let ((file (file-name-nondirectory filename)))
(if (and (string-match "\\.[^.]*\\'" file)
(not (eq 0 (match-beginning 0))))
(substring file (+ (match-beginning 0) 1))))))
(defun python-django-util-shell-command-to-string (command)
"Execute shell COMMAND and return its output as a string.
Returns a cons cell where the car is the exit status and the cdr
is the captured output."
(with-temp-buffer
(cons
(apply 'call-process shell-file-name
nil t nil (list shell-command-switch command))
(buffer-string))))
(defun python-django-util-shell-command-or-error (command)
"Execute shell COMMAND and return its output as a string.
If the exit status is an error `python-django-error-log' is used
to display command output."
(let* ((result (python-django-util-shell-command-to-string command))
(status (car result))
(output (cdr result)))
(if (zerop status)
output
(python-django-error-log
(concat "Error executing: " command "\n\n" output)))))
(defun python-django-util-shorten-settings (&optional settings)
"Return a shorter SETTINGS module string.
Optional Argument SETTINGS defaults to the value of
`python-django-project-settings'."
(or settings (setq settings python-django-project-settings))
(let ((beg (string-match "settings\\." settings)))
(if beg
(substring
settings
(+ beg (length (match-string-no-properties 0 settings))))
settings)))
;;; Help
(defun python-django--help-get (&optional command)
"Get help for COMMAND."
(let* ((process-environment
(python-django-info-calculate-process-environment))
(exec-path (python-shell-calculate-exec-path)))
(python-django-util-shell-command-or-error
;; "--help" is better than "help" because it won't make the command end
;; with failure on Django<1.4.
(format "%s %s %s--help"
(executable-find python-shell-interpreter)
python-django-project-manage.py
(if command (format "%s " command) "")))))
(defun python-django-help (&optional command show-help)
"Get help for given COMMAND.
Optional argument SHOW-HELP when non-nil causes the help buffer to pop."
(interactive
(list
(python-django-minibuffer-read-command)))
(if (or show-help (called-interactively-p 'interactive))
(with-help-window (help-buffer)
(princ (python-django--help-get command)))
(python-django--help-get command)))
(defun python-django-help-close ()
"Close help window if visible."
(let ((win (get-buffer-window (help-buffer))))
(and win
(delete-window win))))
;;; Project info
(defun python-django-info-calculate-process-environment ()
"Calculate process environment given current Django project."
(let* ((process-environment (python-shell-calculate-process-environment))
(pythonpath (getenv "PYTHONPATH"))
(project-pythonpath
(mapconcat
'identity
(list (expand-file-name python-django-project-root)
(expand-file-name "../" python-django-project-root))
path-separator)))
(setenv "PYTHONPATH" (if (not pythonpath)
project-pythonpath
(format "%s%s%s"
pythonpath
path-separator
project-pythonpath)))
(setenv "DJANGO_SETTINGS_MODULE"
python-django-project-settings)
process-environment))
(defun python-django-info-find-manage.py (&optional dir)
"Find manage.py script starting from DIR."
(let ((dir (expand-file-name (or dir default-directory))))
(if (not (directory-files dir nil "^manage\\.py$"))
(and
;; Check dir is not directory root.
(not (string-equal "/" dir))
(not
(and (memq system-type '(windows-nt ms-dos))
(string-match "\\`[a-zA-Z]:[/\\]\\'" dir)))
(python-django-info-find-manage.py
(expand-file-name
(file-name-as-directory "..") dir)))
(expand-file-name "manage.py" dir))))
(defvar python-django-info-prefetched-settings
'("INSTALLED_APPS" "DATABASES" "MEDIA_ROOT" "STATIC_ROOT" "TEMPLATE_DIRS"
"STATICFILES_DIRS"))
(defvar python-django-info--get-setting-cache nil
"Alist with cached list of settings.")
(defvar python-django-info--get-version-cache nil
"Alist with cached list of settings.")
(defun python-django-info-get-version (&optional force)
"Get current Django version path.
Values retrieved by this function are cached so when FORCE is
non-nil the cached value is invalidated."
(or
(and (not force) python-django-info--get-version-cache))
(setq
python-django-info--get-version-cache
(let* ((process-environment
(python-django-info-calculate-process-environment))
(exec-path (python-shell-calculate-exec-path)))
(python-django-util-shell-command-or-error
(format
"%s -c \"%s\""
(executable-find python-shell-interpreter)
(concat
"from __future__ import print_function\n"
"import django\n"
"print(django.get_version(), end='')"))))))
(defvar python-django-info-imports-code
(concat "\n"
"from __future__ import print_function\n"
"import os\n"
"import sys\n"
"from os.path import dirname, abspath\n"
"stdout = sys.stdout; stderr = sys.stderr\n"
"sys.stdout = sys.stderr = open(os.devnull, 'w')\n"
"from django.conf import settings\n"
"# Try to import json really hard\n"
"try:\n"
" import json\n"
"except ImportError:\n"
" from django.utils import simplejson as json\n"
"# Force settings loading so all output is sent to devnull.\n"
"settings.DEBUG\n"
"sys.stdout = stdout; sys.stderr = stderr\n\n")
"All imports code used to get info.
It contains output redirecting features so settings import
doesn't break the JSON output.")
(defun python-django-info-get-settings (&optional force)
"Prefretch most common used settings for project.
Values retrieved by this function are cached so when FORCE is
non-nil the cached value is invalidated."
(let ((cached
(mapcar
#'(lambda (setting)
(assq (intern setting)
python-django-info--get-setting-cache))
python-django-info-prefetched-settings)))
(if (and (not force)
(catch 'exit
(dolist (elt cached)
(when (null elt)
(throw 'exit nil)))
t))
cached
(let* ((process-environment
(python-django-info-calculate-process-environment))
(exec-path (python-shell-calculate-exec-path))
(settings-list-string
(concat "["
(mapconcat
#'(lambda (str) (concat "'" str "'"))
python-django-info-prefetched-settings
", ")
"]"))
(value
(json-read-from-string
(python-django-util-shell-command-or-error
(format "%s -c \"%s%s\""
(executable-find python-shell-interpreter)
python-django-info-imports-code
(concat
"acc = {}\n"
"for name in " settings-list-string ":\n"
" acc[name] = getattr(settings, name, None)\n"
"print(json.dumps(acc), end='')"))))))
(mapc
(lambda (elt)
(let ((cached-val
(assq (car elt) python-django-info--get-setting-cache)))
(if cached-val
(setcdr cached-val (cdr elt))
(setq python-django-info--get-setting-cache
(cons elt python-django-info--get-setting-cache)))))
value)))))
(defun python-django-info-get-setting (setting &optional force)
"Get SETTING value from django.conf.settings in JSON format.
Values retrieved by this function are cached so when FORCE is
non-nil the cached value is invalidated."
(let ((cached
(or (and
(member setting python-django-info-prefetched-settings)
(assq (intern setting) (python-django-info-get-settings force)))
(assq (intern setting)
python-django-info--get-setting-cache))))
(if (and (not force) cached)
(cdr cached)
(let* ((process-environment
(python-django-info-calculate-process-environment))
(exec-path (python-shell-calculate-exec-path))
(value
(json-read-from-string
(python-django-util-shell-command-or-error
(format
"%s -c \"%s%s\""
(executable-find python-shell-interpreter)
python-django-info-imports-code
(format
(concat
"print(json.dumps("
"getattr(settings, '%s', None)), end='')")
setting)))))
(already-cached (assq (intern setting)
python-django-info--get-setting-cache)))
(if already-cached
(setcdr already-cached value)
(setq python-django-info--get-setting-cache
(cons (cons (intern setting) value)
python-django-info--get-setting-cache)))
value))))
(defvar python-django-info--get-app-paths-cache nil
"Cached list of apps and paths.")
(defun python-django-info-get-app-paths (&optional force)
"Get project paths path.
Values retrieved by this function are cached so when FORCE is
non-nil the cached value is invalidated."
(if (or force (not python-django-info--get-app-paths-cache))
(setq
python-django-info--get-app-paths-cache
(let* ((process-environment
(python-django-info-calculate-process-environment))
(exec-path (python-shell-calculate-exec-path)))
(json-read-from-string
(python-django-util-shell-command-or-error
(format "%s -c \"%s%s\""
(executable-find python-shell-interpreter)
python-django-info-imports-code
"
try:
from django.apps import apps
except ImportError:
# Django<1.7 app loading.
import os.path
from django.utils.importlib import import_module
app_paths = {}
for app_string in settings.INSTALLED_APPS:
app_module = import_module(app_string)
app_path = os.path.dirname(app_module.__file__)
# Keep the last part (e.g: 'django.contrib.admin' -> 'admin')
app_label = app_string.rpartition('.')[2]
app_paths[app_label] = app_path
else:
# Django>=1.7 app loading.
apps.populate(installed_apps=settings.INSTALLED_APPS)
app_paths = {app.label: app.path for app in apps.get_app_configs()}
print(json.dumps(app_paths), end='')")))))
python-django-info--get-app-paths-cache))
(defun python-django-info-get-app-path (app &optional force)
"Get APP's path.
Values retrieved by this function are cached so when FORCE is
non-nil the cached value is invalidated."
(cdr (assq (intern app) (python-django-info-get-app-paths force))))
(defun python-django-info-get-installed-apps (&optional force)
"Get list of strings of installed app labels.
Values retrieved by this function are cached so when FORCE is
non-nil the cached value is invalidated."
(mapcar
(lambda (elt)
(symbol-name (car elt)))
(python-django-info-get-app-paths force)))
(defun python-django-info-get-app-migrations (app)
"Get APP's list of migrations."
(mapcar (lambda (file)
file)
(ignore-errors
(directory-files
(expand-file-name
"migrations"
(python-django-info-get-app-path app))
nil "^[0-9]\\{4\\}_.*\\.py$"))))
(defun python-django-info-module-path (module)
"Get MODULE's path."
(let* ((process-environment
(python-django-info-calculate-process-environment))
(exec-path (python-shell-calculate-exec-path)))
(python-django-util-shell-command-or-error
(format "%s -c \"%s%s%s\""
(executable-find python-shell-interpreter)
python-django-info-imports-code
(format "import %s\n" module)
(format
"print(%s.__file__.replace('.pyc', '.py'), end='')" module)))))
(defun python-django-info-directory-basename (&optional dir)
"Get innermost directory name for given DIR."
(car (last (split-string dir "/" t))))
;;; Hippie expand completion
(defun python-django-minibuffer-try-complete-args (old)
"Try to complete word as a management command argument.
The argument OLD has to be nil the first call of this function, and t
for subsequent calls (for further possible completions of the same
string). It returns t if a new completion is found, nil otherwise."
(save-excursion
(unless old
(he-init-string (he-dabbrev-beg) (point))
(when (not (equal he-search-string ""))
(setq he-expand-list
(sort (all-completions
he-search-string
minibuffer-completion-table)
'string<))))
(while (and he-expand-list
(he-string-member (car he-expand-list) he-tried-table))
(setq he-expand-list (cdr he-expand-list)))
(if (null he-expand-list)
(progn (if old (he-reset-string)) ())
(progn
(he-substitute-string (car he-expand-list))
(setq he-tried-table (cons (car he-expand-list)
(cdr he-tried-table)))
t))))
(defun python-django-minibuffer-try-complete-filenames (old)
"Try to complete filenames in command arguments.
The argument OLD has to be nil the first call of this function, and t
for subsequent calls (for further possible completions of the same
string). It returns t if a new completion is found, nil otherwise."
(if (not old)
(progn
(he-init-string (let ((max-point (point)))
(save-excursion
(goto-char (he-file-name-beg))
(re-search-forward "--?[a-z0-9_-]+=?" max-point t)
(point)))
(point))
(let ((name-part (file-name-nondirectory he-search-string))
(dir-part (expand-file-name (or (file-name-directory
he-search-string) ""))))
(if (not (he-string-member name-part he-tried-table))
(setq he-tried-table (cons name-part he-tried-table)))
(if (and (not (equal he-search-string ""))
(file-directory-p dir-part))
(setq he-expand-list (sort (file-name-all-completions
name-part
dir-part)
'string-lessp))
(setq he-expand-list ())))))
(while (and he-expand-list
(he-string-member (car he-expand-list) he-tried-table))
(setq he-expand-list (cdr he-expand-list)))
(if (null he-expand-list)
(progn
(if old (he-reset-string))
())
(let ((filename (he-concat-directory-file-name
(file-name-directory he-search-string)
(car he-expand-list))))
(he-substitute-string filename)
(setq he-tried-table (cons (car he-expand-list) (cdr he-tried-table)))
(setq he-expand-list (cdr he-expand-list))
t)))
;;; Minibuffer
(defvar python-django-minibuffer-complete-command-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map minibuffer-local-must-match-map)
map)
"Keymap used for completing commands in minibuffer.")
(defvar python-django-minibuffer-complete-command-args-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map minibuffer-local-map)
(define-key map "\t" 'hippie-expand)
(define-key map [remap scroll-other-window]
'python-django-minibuffer-scroll-help-window)
(define-key map [remap scroll-other-window-down]
'python-django-minibuffer-scroll-help-window-down)
map)
"Keymap used for completing command args in minibuffer.")
(defun python-django-minibuffer-read-command (&optional trigger-help)
"Read django management command from minibuffer.
Optional argument TRIGGER-HELP sets if help buffer with commmand
details should be displayed."
(let* ((current-buffer (current-buffer))
(command
(minibuffer-with-setup-hook
(lambda ()
(python-util-clone-local-variables current-buffer)
(setq minibuffer-completion-table
(python-django-mgmt-list-commands)))
(read-from-minibuffer
"./manage.py: " nil
python-django-minibuffer-complete-command-map))))
(when trigger-help
(python-django-help command t))
command))
(defun python-django-minibuffer-read-command-args (command)
"Read django management arguments for command from minibuffer.
Arguments are parsed for especific COMMAND."
(let* ((current-buffer (current-buffer)))
(minibuffer-with-setup-hook
(lambda ()
(python-util-clone-local-variables current-buffer)
(setq minibuffer-completion-table
(python-django-mgmt-list-command-args command))
(set (make-local-variable 'hippie-expand-try-functions-list)
'(python-django-minibuffer-try-complete-args
python-django-minibuffer-try-complete-filenames)))
(read-from-minibuffer
(format "./manage.py %s (args): " command)
nil python-django-minibuffer-complete-command-args-map))))
(defun python-django-minibuffer-read-list (thing &rest args)
"Helper function to read list of THING from minibuffer.
Optional argument ARGS are the args passed to the THING."
(let ((objs))
(catch 'exit
(while t
(add-to-list
'objs
(apply thing args) t)
(when (not (y-or-n-p "Add another? "))
(throw 'exit (mapconcat 'identity objs " ")))))))
(defun python-django-minibuffer-read-file-name (prompt)
"Read a single file name from minibuffer.
PROMPT is a string to prompt user for filenames."
(let ((use-dialog-box nil))
;; Lets make shell expansion work.
(replace-regexp-in-string
"[\\]\\*" "*"
(shell-quote-argument
(let ((func
(if ido-mode
'ido-read-file-name
'read-file-name)))
(funcall func prompt python-django-project-root
python-django-project-root nil))))))
(defun python-django-minibuffer-read-file-names (prompt)
"Read a list of file names from minibuffer.
PROMPT is a string to prompt user for filenames."
(python-django-minibuffer-read-list
'python-django-minibuffer-read-file-name prompt))
(defun python-django-minibuffer-read-app (prompt &optional initial-input)
"Read django app from minibuffer.
PROMPT is a string to prompt user for app. Optional argument
INITIAL-INPUT is the initial prompted value."
(let ((apps (python-django-info-get-installed-apps))
(current-buffer (current-buffer)))
(minibuffer-with-setup-hook
(lambda ()
(python-util-clone-local-variables current-buffer)
(setq minibuffer-completion-table apps))
(catch 'app
(while t
(let ((app (read-from-minibuffer
prompt initial-input minibuffer-local-must-match-map)))
(when (> (length app) 0)
(throw 'app app))))))))
(defun python-django-minibuffer-read-apps (prompt &optional initial-input)
"Read django apps from minibuffer.
PROMPT is a string to prompt user for app. Optional argument
INITIAL-INPUT is the initial prompted value."
(python-django-minibuffer-read-list
'python-django-minibuffer-read-app prompt))
(defun python-django-minibuffer-read-database (prompt &optional initial-input)
"Read django database router name from minibuffer.
PROMPT is a string to prompt user for database.
Optional argument INITIAL-INPUT is the initial prompted value."
(let ((databases (mapcar (lambda (router)
(format "%s" (car router)))
(python-django-info-get-setting "DATABASES")))
(current-buffer (current-buffer)))
(minibuffer-with-setup-hook
(lambda ()
(python-util-clone-local-variables current-buffer)
(setq minibuffer-completion-table databases))
(catch 'db
(while t
(let ((db (read-from-minibuffer
prompt initial-input minibuffer-local-must-match-map)))
(when (> (length db) 0)
(throw 'db db))))))))
(defun python-django-minibuffer-read-migration (prompt app)
"Read south migration number for given app from minibuffer.
PROMPT is a string to prompt user for database. APP is the app
to read migrations from."
(let* ((migrations (python-django-info-get-app-migrations app)))
(minibuffer-with-setup-hook
(lambda ()
(setq minibuffer-completion-table migrations))
(let ((migration (read-from-minibuffer
prompt nil minibuffer-local-must-match-map)))
(when (not (string= migration ""))
(substring migration 0 4))))))
(defun python-django-minibuffer-read-from-list (prompt lst &optional default)
"Read a value from a list from minibuffer.
PROMPT is a string to prompt user. LST is the list containing
the values to choose from. Optional argument DEFAULT is the
default value."
(minibuffer-with-setup-hook
(lambda ()
(setq minibuffer-completion-table lst))
(read-from-minibuffer prompt default minibuffer-local-must-match-map)))
;;; Management commands
(defvar python-django-mgmt--available-commands nil
"Alist with cached list of management commands for each project.")
(defun python-django-mgmt-list-commands (&optional force)
"List available management commands.
Optional argument FORCE makes the function to recalculate the
list of command for current project instead of getting it from
the `python-django-mgmt--available-commands' cache."
(and force
(set (make-local-variable 'python-django-mgmt--available-commands) nil))
(cdr
(or python-django-mgmt--available-commands
(let ((help-string (python-django-help))
(commands))
(set (make-local-variable 'python-django-mgmt--available-commands)
(with-temp-buffer
(insert help-string)
(goto-char (point-min))
(re-search-forward "Available subcommands:\n")
(delete-region (point-min) (point))
(while (re-search-forward " +\\([a-z0-9_]+\\)\n" nil t)
(setq commands
(cons (match-string-no-properties 1) commands)))
(reverse commands)))))))
(defun python-django-mgmt-list-command-args (command)
"List available arguments for COMMAND."
(let ((help-string (python-django-help command))
(args))
(with-temp-buffer
(insert help-string)
(goto-char (point-min))
(when (re-search-forward "^Options:\n" nil t)
(while (re-search-forward "--[a-z0-9_-]+=?" nil t)
(setq args (cons (match-string 0) args))
(append args (match-string 0)))
(sort args 'string<)))))
(defun python-django-mgmt-make-comint (command process-name)
"Run COMMAND with PROCESS-NAME in generic Comint buffer."
(apply 'make-comint process-name
(executable-find python-shell-interpreter) nil
(split-string-and-unquote command)))
(defun python-django-mgmt-make-comint-for-shell (command process-name)
"Run COMMAND with PROCESS-NAME in generic Comint buffer."
(let ((python-shell-interpreter-args command))
(python-shell-make-comint (python-shell-parse-command) process-name)))
(defun python-django-mgmt-make-comint-for-runserver (command process-name)
"Run COMMAND with PROCESS-NAME in generic Comint buffer."
(let ((python-shell-enable-font-lock nil))
(python-django-mgmt-make-comint-for-shell command process-name)))
(defun python-django-mgmt-make-comint-for-dbshell (command process-name)
"Run COMMAND with PROCESS-NAME in generic Comint buffer."
(let* ((dbsetting (python-django-info-get-setting "DATABASES"))
(dbengine (cdr (assoc 'ENGINE (assoc 'default dbsetting))))
(sql-interactive-product-1
(cond ((string= dbengine "django.db.backends.mysql")
'mysql)
((string= dbengine "django.db.backends.oracle")
'oracle)
((string= dbengine "django.db.backends.postgresql")
'postgres)
((string= dbengine "django.db.backends.sqlite3")
'sqlite)
(t nil)))
(buffer
(python-django-mgmt-make-comint command process-name)))
(with-current-buffer buffer
(setq sql-buffer (current-buffer)
sql-interactive-product sql-interactive-product-1)
(sql-interactive-mode))
buffer))
(defcustom python-django-mgmt-buffer-switch-function 'display-buffer
"Function for switching to the process buffer.
The function receives one argument, the management command
process buffer."
:group 'python-django
:type '(radio (function-item switch-to-buffer)
(function-item pop-to-buffer)
(function-item display-buffer)
(function :tag "Other")))
(defvar python-django-mgmt--previous-window-configuration nil
"Snapshot of previous window configuration before executing command.
This variable is for internal purposes, don't use it directly.")
(defun python-django-mgmt-restore-window-configuration ()
"Restore window configuration after running a management command."
(and python-django-mgmt--previous-window-configuration
(set-window-configuration
python-django-mgmt--previous-window-configuration)))
(defvar python-django-mgmt-parent-buffer nil
"Parent project buffer for current process.")
(defvar python-django-mgmt--opened-buffers nil
"Alist of currently opened process buffers.")
(defun python-django-mgmt-buffer-list (&optional parent-buffer)
"Return all opened buffer names for PARENT-BUFFER.
Optional Argument PARENT-BUFFER defaults to the current buffer."
(python-django-util-alist-get
(or parent-buffer (current-buffer))
python-django-mgmt--opened-buffers))
(defvar python-django-mgmt--buffer-index 0)
(defun python-django-mgmt-buffer-get (&optional index)
"Get management buffer by INDEX.
Optional Argument INDEX defaults to the value of
`python-django-mgmt--buffer-index'."
(let ((buffer-list (python-django-mgmt-buffer-list)))
(and buffer-list
(nth (mod (or index python-django-mgmt--buffer-index)
(length buffer-list)) buffer-list))))
(defun python-django-mgmt-cycle-buffers-forward (&optional arg)
"Cycle opened process buffers forward.
With Optional Argument ARG cycle that many buffers."
(interactive "p")
(setq arg (or arg 1))
(let ((buffers (python-django-mgmt-buffer-list)))
(and buffers
(let ((newindex
(mod (+ python-django-mgmt--buffer-index arg)
(length buffers))))
(set (make-local-variable
'python-django-mgmt--buffer-index) newindex)
(display-buffer (nth newindex buffers))))))
(defun python-django-mgmt-cycle-buffers-backward (&optional arg)
"Cycle opened process buffers backward.
With Optional Argument ARG cycle that many buffers."
(interactive "p")
(python-django-mgmt-cycle-buffers-forward (- (or arg 1))))
(defun python-django-mgmt-run-command (command
&optional args capture-ouput no-pop
make-comint-function)
"Run management COMMAND with given ARGS.
When optional argument CAPTURE-OUPUT is non-nil process output is
not truncated by the `comint-truncate-buffer' output filter. If
optional argument NO-POP is provided the process buffer is not
displayed automatically. When optional argument
MAKE-COMINT-FUNCTION is non-nil use that function to create the
comint process, defaults to `python-django-mgmt-make-comint'."
(interactive
(list
(setq command
(python-django-minibuffer-read-command t))
(python-django-minibuffer-read-command-args command)))
(python-django-help-close)
(when (not (member command (python-django-mgmt-list-commands)))
(error
"Management command %s is not available in current project" command))
(let* ((args (or args ""))
(process-environment
(python-django-info-calculate-process-environment))
(exec-path (python-shell-calculate-exec-path))
(process-name
(replace-regexp-in-string
"[\t ]+$" ""
(format "[Django: %s (%s)] ./manage.py %s %s"
python-django-project-name
(python-django-util-shorten-settings)
command args)))
(buffer-name (format "*%s*" process-name))
(current-buffer (current-buffer))
(make-comint-function (or make-comint-function
#'python-django-mgmt-make-comint))
(full-command
(format "%s %s %s"
python-django-project-manage.py
command args)))
(funcall make-comint-function full-command process-name)
(with-current-buffer buffer-name
(python-util-clone-local-variables current-buffer)
(and (not capture-ouput)
(add-hook 'comint-output-filter-functions
'comint-truncate-buffer nil t))
(set (make-local-variable
'python-django-mgmt-parent-buffer) current-buffer)
(python-django-util-alist-add
current-buffer (current-buffer)
python-django-mgmt--opened-buffers)
(add-hook
'kill-buffer-hook
(lambda ()
(python-django-util-alist-del
python-django-mgmt-parent-buffer (current-buffer)
python-django-mgmt--opened-buffers))
nil t))
(unless no-pop
(funcall python-django-mgmt-buffer-switch-function buffer-name)
(with-current-buffer buffer-name
(and (get-buffer-process (current-buffer))
(comint-goto-process-mark))))
buffer-name))
(add-to-list 'debug-ignored-errors
"^Management command .* is not available in current project.")
(defun python-django-mgmt-kill (&optional buffer)
"Kill current command's BUFFER."
(interactive)
(setq buffer (or buffer (python-django-mgmt-buffer-get)))
(when (and buffer (or (not (called-interactively-p 'any))
(y-or-n-p
(format "Kill %s? " buffer))))
(let ((win (get-buffer-window buffer 0))
(proc (get-buffer-process buffer)))
(and win (delete-window win))
(and proc (set-process-query-on-exit-flag proc nil))
(kill-buffer buffer)
(python-django-mgmt-cycle-buffers-forward))))
(defun python-django-mgmt-kill-all (&optional command)
"Kill all running commands for current project after CONFIRM.
When called with universal argument you can filter the COMMAND to kill."
(interactive
(list
(and current-prefix-arg
(python-django-minibuffer-read-command nil))))
(when (or (not (called-interactively-p 'any))
(y-or-n-p
(format "Do you want to kill all running commands for %s? "
python-django-project-name)))
(dolist (buffer
(python-django-mgmt-buffer-list (current-buffer)))
(when (or (not command)
(string-match
(format "\\./manage.py %s" (or command "")) buffer))
(let ((win (get-buffer-window buffer 0))
(proc (get-buffer-process buffer)))
(and win (delete-window win))
(and proc (set-process-query-on-exit-flag proc nil)))
(kill-buffer buffer)))))
;;; Management shortcuts
(eval-and-compile
(defun python-django-qmgmt--make-fn-symbol (name)
"Return a quick management command defun symbol from NAME."
(intern (format "python-django-qmgmt-%s" name)))
(defun python-django-qmgmt--make-functions-symbol (name)
"Return a quick management command functions symbol from NAME."
(intern (format "python-django-qmgmt-%s-functions" name)))
(defun python-django-qmgmt--make-spec
(command &optional switches interactive-switches)
"Return human readable spec for COMMAND.
The spec is the shell command with placeholders in it. Example:
./manage.py cmd --all --database=<database> --app=<app>
Where \"--all\" is SWITCHES value, while database and app are
generated from INTERACTIVE-SWITCHES."
(concat
(format "./manage.py %s " command)
(unless (zerop (length switches))
(format "%s " switches))
(when interactive-switches
(mapconcat
(lambda (arg)
(let* ((switch (nth 3 arg))
(switch
(cond
((eq (length switch) 0)
"")
((eq ?= (car (last (append switch nil))))
switch)
(t (format "%s " switch))))
(varname (symbol-name (nth 0 arg))))
(format "%s<%s>" switch varname)))
interactive-switches
" "))))
(defun python-django-qmgmt--make-docstring
(command &optional switches interactive-switches docstring)
"Return documentation string for auto-generated command.
Arguments COMMAND, SWITCHES, INTERACTIVE-SWITCHES and DOCSTRING
are used to generate the string."
(format
"%s\n\n%s"
;; Show either the spec, or the docstring *and* the spec.
(concat
(unless (zerop (length docstring))
(concat docstring "\n\n"))
"Run: "
(python-django-qmgmt--make-spec
command switches interactive-switches))
(concat
"This is an interactive command defined via "
"`python-django-qmgmt-define' macro.\n"
"Parameter defaults can be overriden by "
"calling this command with `prefix-arg' .\n\n"
(when switches
(format "Default switches: \n\n * %s\n\n" switches))
(when interactive-switches
(format
"Arguments: \n\n%s"
(mapconcat
(lambda (arg)
(let* ((default (nth 2 arg))
(switch (nth 3 arg))
(switch
(cond
((eq (length switch) 0)
nil)
((eq ?= (car (last (append switch nil))))
switch)
(t (format "%s " switch))))
(force-ask (nth 4 arg)))
(concat
(format " * %s:\n"
(upcase (symbol-name (car arg))))
(format " + Switch: %s\n" switch)
(format " + Defaults: %s\n"
(prin1-to-string default))
(format " + Read SPEC: %s\n"
(prin1-to-string (nth 1 arg)))
(format " + Force prompt: %s\n"
force-ask)
(format " + Requires user interaction?: %s"
(if (or force-ask (not default))
"yes" "no")))))
interactive-switches "\n\n"))))))
(defun python-django-qmgmt--make-functions-docstring
(fn-symbol)
"Return documentation string for auto-generated functions variable.
Argument FN-SYMBOL is the symbol of the command."
(format
"A function or list of called after `%s's bound process finishes.
Functions defined here are called in order and must receive two
arguments: The first must be a STATUS-OK which is non-nil if the
process exited successfully and ARGS which is a plist with the
switches and arguments given to it. See
`python-django-qmgmt-define' docstring for details." fn-symbol))
(defun python-django-qmgmt--make-interactive-form (interactive-switches)
"Return interactive form from INTERACTIVE-SWITCHES."
(mapcar
(lambda (arg)
;; Interactive switch form: (VARNAME PROMPT DEFAULT SWITCH FORCE-ASK)
(let* ((default (nth 2 arg))
(switch (nth 3 arg))
(switch
(cond ((zerop (length switch))
;; This is a positional argument. Skip prefix.
"")
((eq ?= (car (last (append switch nil))))
;; Long switch (e.g "--database="), keep it as prefix.
switch)
;; Shor switch (e.g "-d"), add space suffix.
(t (format "%s " switch))))
(force-ask (nth 4 arg))
(read-func
(if (stringp (nth 1 arg))
;; Use string as prompt.
`(read-string ,(nth 1 arg) default)
;; Execute form to read argument value.
(nth 1 arg))))
`(concat
,switch
(setq ,(car arg)
(let ((default ,default))
(if (or ,force-ask
current-prefix-arg
(not default))
,read-func
default))))))
interactive-switches))
(defun python-django-qmgmt--add-binding (symbol &optional binding)
"Add keybinding to SYMBOL using BINDING."
(when binding
(ignore-errors
(define-key python-django-mode-map (concat "c" binding) symbol))))
(defun python-django-qmgmt--add-menu-item
(symbol submenu command &optional switches interactive-switches docstring)
"Add menu item for SYMBOL in SUBMENU to execute mangament COMMAND.
Arguments SWITCHES, INTERACTIVE-SWITCHES and DOCSTRING are used
to generate a descriptive item."
(let* ((spec (python-django-qmgmt--make-spec
command switches interactive-switches))
(help (if (zerop (length docstring)) ; also handle empty string
(format "Run ./manage.py %s" spec)
(car (split-string docstring "\n")))))
(easy-menu-add-item
'python-django-menu nil (list submenu) "---")
(easy-menu-add-item
'python-django-menu (list submenu)
`[,spec
,symbol
:help ,help
:active (member ,command (python-django-mgmt-list-commands))]
"---"))))
(defmacro python-django-qmgmt-define (name doc-or-args
&optional args &rest interactive-switches)
"Define a quick management command.
Argument NAME is a symbol and it is used to calculate the
management command this command will execute, so it should have
the form cmdname[-rest]. Argument DOC-OR-ARGS might be the
docstring for the defined command or the list of arguments, when
a docstring is supplied ARGS is used as the list of arguments
instead. The rest INTERACTIVE-SWITCHES is a list of interactive
switches the user will be prompted for.
This is a full example that will define how to execute Django's
dumpdata for the current application quickly:
(python-django-qmgmt-define dumpdata-app
\"Run dumpdata for current application.\"
(:submenu \"Database\" :switches \"--format=json\" :binding \"dda\")
(database \"Database\" \"default\" \"--database=\")
(app \"App\"))
When that's is evaled a command called
`python-django-qmgmt-dumpdata-app' is created and will react
depending on the arguments passed to this macro.
All commands defined by this macro, when called with `prefix-arg'
will ask the user for values instead of using defaults.
ARGS is a property list. Valid keys are (all optional):
+ :binding, when defined, the new command is bound to the
default prefix for quick management commands plus this value.
+ :capture-output, when non-nil, the command output is not
truncated by the `comint-truncate-buffer' output filter.
+ :msg, when defined, commands that use the
`python-django-qmgmt-kill-and-msg-function' show this instead
of the buffer contents.
+ :no-pop, when non-nil, causes the process buffer to not be
displayed.
+ :submenu, when defined, the quick management command is
added within that submenu tree. If omitted the item is added
to the root.
+ :switches, when defined, the new command is executed with
these fixed switches.
+ :make-comint-function, a function to be used to create the
comint process, defaults to `python-django-mgmt-make-comint'.
+ :functions, If the value is a function, it is called with
two arguments. If it is a list, the elements are called, in
order, with same two arguments each. The first argument is
non-nil if the process ended successfully; the second
argument is a property-list containing passed ARGS and
INTERACTIVE-SWITCHES with symbols as keys. E.g: '(app
\"auth\" :command \"test\").
If you define any extra keys they will not be taken into account
by this macro but you may well use them in your command's
callback.
INTERACTIVE-SWITCHES have the form (VARNAME PROMPT DEFAULT SWITCH
FORCE-ASK), you can add 0 or more INTERACTIVE-SWITCHES depending
on the number of parameters you need to pass to the management
command. The description for each element of the list are:
+ VARNAME must be a unique symbol not used in other switch.
+ PROMPT must be a string for the prompt that will be shown
when user is asked for a value using `read-string' or it can
be a expresion that will be used to read the value for
VARNAME. When you need to use the calculated value of
DEFAULT in the provided expression you can just use that
variable like this:
(read-file-name \"Fixture: \" nil default)
+ DEFAULT is an expression to be executed in order to
calculate the default value for VARNAME. This is optional
and in the case is not provided or returns nil after executed
the user will be prompted to insert a value for VARNAME.
+ SWITCH is a string that represents the switch used to pass
the VARNAME's value to Django's management command.
+ FORCE-ASK might be nil or non-nil, when is non-nil the user
will be asked to insert a value for VARNAME even if a default
value is available.
Each command defined via this macro may have a callback to be
executed when the process finishes correctly. The way to define
callbacks is to append -callback to the defined name, for
instance if you defined a quick management command called syncdb,
then you need to create a function named
`python-django-qmgmt-syncdb-callback' and it will be called with
an alist containing all INTERACTIVE-SWITCHES and ARGS with the
additional :command key holding the executed command. See the
`python-django-qmgmt-kill-and-msg-function' function for a nice
example of a callback."
(declare (indent defun))
(let* ((docstring (when (stringp doc-or-args) doc-or-args))
(args (if docstring args doc-or-args))
(args (if (eq ?: (string-to-char (symbol-name (car args))))
args
;; args is not a plist, append it to interactive-switches and
;; set args to nil.
(setq interactive-switches (cons args interactive-switches))
nil))
(fn-symbol (python-django-qmgmt--make-fn-symbol name))
(callback (intern (format "%s-callback" fn-symbol)))
(command (car (split-string (format "%s" name) "-")))
(binding (plist-get args :binding))
(capture-output (plist-get args :capture-output))
(functions (plist-get args :functions))
(no-pop (plist-get args :no-pop))
(submenu (plist-get args :submenu))
(switches (plist-get args :switches))
(make-comint-function (plist-get args :make-comint-function))
(defargs (mapcar 'car interactive-switches))
;; Abnormal hooks AKA functions
(functions-symbol (python-django-qmgmt--make-functions-symbol name))
(functions-docstring
(python-django-qmgmt--make-functions-docstring fn-symbol)))
`(progn
(defvar ,functions-symbol nil
,functions-docstring)
(setq ,functions-symbol ,functions)
(defun ,fn-symbol ,defargs
,(python-django-qmgmt--make-docstring
command switches interactive-switches docstring)
(interactive
(list ,@(python-django-qmgmt--make-interactive-form
interactive-switches)))
(setq python-django-mgmt--previous-window-configuration
(current-window-configuration))
(let* ((cmd-args (concat ,switches (and ,switches " ")
(mapconcat #'symbol-value ',defargs " ")))
(process (get-buffer-process
(python-django-mgmt-run-command
,command cmd-args
,capture-output ,no-pop
,make-comint-function))))
(message "Running: ./manage.py %s %s" ,command cmd-args)
(set-process-sentinel
process
(lambda (process status)
(when (and (not (process-live-p process))
(buffer-live-p (process-buffer process)))
(with-current-buffer (process-buffer process)
(run-hook-with-args
',functions-symbol
(string= status "finished\n")
(let ((plist ',args))
(plist-put plist :command ,command)
(mapc
#'(lambda (sym)
(message "[%s]" sym)
(let* ((cmd-switch (symbol-value sym))
(value
(cond
((string-match "^--" cmd-switch)
(substring
cmd-switch
(1+ (string-match "=" cmd-switch))))
((string-match "^-" cmd-switch)
(substring cmd-switch 3))
(t cmd-switch))))
(plist-put plist sym value)))
',defargs)
plist))))))))
(python-django-qmgmt--add-binding #',fn-symbol ,binding)
(python-django-qmgmt--add-menu-item
#',fn-symbol ,submenu ,command ,switches ',interactive-switches ,docstring)
;; Just like defun, return the defined function.
#',fn-symbol)))
(defun python-django-qmgmt-kill-and-msg-function (status-ok args)
"Kill the process buffer and show message or output.
Argument STATUS-OK is non-nil if process exited successfully.
Argument ARGS is a plist with the switches and arguments passed
to the command. See `python-django-qmgmt-define' docstring for
details."
(when status-ok
(let ((msg (or (plist-get args :msg)
(buffer-substring-no-properties
(point-min) (point-max))))
(buffer-name (buffer-name)))
(kill-buffer)
(python-django-mgmt-restore-window-configuration)
(display-message-or-buffer msg buffer-name))))
(python-django-qmgmt-define collectstatic
"Collect static files."
(:submenu "Tools" :binding "ocs"
:functions #'python-django-qmgmt-kill-and-msg-function))
(python-django-qmgmt-define clean_pyc
"Remove all python compiled files from the project."
(:submenu "Tools" :binding "ocp" :no-pop t
:msg "All *.pyc and *.pyo cleaned."
:functions #'python-django-qmgmt-kill-and-msg-function))
(defun python-django-qmgmt-create_command-function (status-ok args)
"Callback for create_command quick management command.
Argument STATUS-OK is non-nil if process exited successfully.
Argument ARGS is a plist with the switches and arguments passed
to the command. See `python-django-qmgmt-define' docstring for
details."
(when status-ok
(let* ((appname (plist-get args 'app))
(manage-directory
(file-name-directory
(with-current-buffer
python-django-mgmt-parent-buffer
python-django-project-manage.py)))
(default-app-dir
(expand-file-name appname manage-directory))
(default-create-dir
(expand-file-name "management" default-app-dir))
(delete-safe
(and (file-exists-p default-app-dir)
(equal (directory-files default-app-dir)
'("." ".." "management")))))
;; TODO: cleanup. Do not offer moving, the command does the right thing.
(when (y-or-n-p
(format "Created in app %s. Move it? " default-app-dir))
(let ((newdir
(read-directory-name
"Move app to: " manage-directory nil t)))
(if (not (file-exists-p
(expand-file-name "management" newdir)))
(rename-file default-create-dir newdir)
(message
"Directory structure already exists in %s" appname))
(and delete-safe (delete-directory default-app-dir t)))))
(kill-buffer)
(python-django-mgmt-restore-window-configuration)))
(python-django-qmgmt-define create_command
"Create management commands directory structure for app."
(:submenu "Tools" :binding "occ" :no-pop t
:functions #'python-django-qmgmt-create_command-function)
(app (python-django-minibuffer-read-app "App name: ")))
(defun python-django-qmgmt-startapp-function (status-ok args)
"Callback for clean_pyc quick management command.
Argument STATUS-OK is non-nil if process exited successfully.
Argument ARGS is a plist with the switches and arguments passed
to the command. See `python-django-qmgmt-define' docstring for
details."
(when status-ok
(let ((appname (plist-get args 'app))
(manage-directory
(file-name-directory
(with-current-buffer
python-django-mgmt-parent-buffer
python-django-project-manage.py))))
(when (y-or-n-p
(format
"App created in %s. Do you want to move it? "
manage-directory))
(rename-file
(expand-file-name appname manage-directory)
(read-directory-name
"Move app to: " manage-directory nil t))))
(kill-buffer)
(python-django-mgmt-restore-window-configuration)))
(python-django-qmgmt-define startapp
"Create new Django app for current project."
(:submenu "Tools" :binding "osa" :no-pop t
:functions #'python-django-qmgmt-startapp-function)
(app "App name: "))
;; Shell
(python-django-qmgmt-define shell
"Run a Python interpreter for this project."
(:submenu "Shell" :binding "ss"
:make-comint-function #'python-django-mgmt-make-comint-for-shell))
(python-django-qmgmt-define shell_plus
"Like the 'shell' but autoloads all models."
(:submenu "Shell" :binding "sp"
:make-comint-function #'python-django-mgmt-make-comint-for-shell))
;; Database
(python-django-qmgmt-define syncdb
"Sync database tables for all INSTALLED_APPS."
(:submenu "Database" :binding "dsy" :no-pop t
:functions #'python-django-qmgmt-kill-and-msg-function)
(database (python-django-minibuffer-read-database "Database: " default)
"default" "--database="))
(python-django-qmgmt-define dbshell
"Run the command-line client for specified database."
(:submenu "Database" :binding "dsh"
:make-comint-function #'python-django-mgmt-make-comint-for-dbshell)
(database (python-django-minibuffer-read-database "Database: " default)
"default" "--database="))
(defvar python-django-qmgmt-dumpdata-formats '("json" "xml" "yaml")
"Valid formats for dumpdata management command.")
(defcustom python-django-qmgmt-dumpdata-default-format "json"
"Default format for quick dumpdata."
:group 'python-django
:type `(choice
,@(mapcar (lambda (fmt)
`(string :tag ,fmt ,fmt))
python-django-qmgmt-dumpdata-formats))
:safe 'stringp)
(defcustom python-django-qmgmt-dumpdata-default-indent 4
"Default indent value quick dumpdata."
:group 'python-django
:type 'integer
:safe 'integerp)
(defun python-django-qmgmt-dumpdata-function (status-ok args)
"Callback executed after dumpdata finishes.
Argument STATUS-OK is non-nil if process exited successfully.
Argument ARGS is a plist with the switches and arguments passed
to the command. See `python-django-qmgmt-define' docstring for
details."
(when status-ok
(let ((file-name
(catch 'file-name
(while t
(let ((file-name
(read-file-name
"Save fixture to file: "
(expand-file-name
(with-current-buffer
python-django-mgmt-parent-buffer
python-django-project-root)) nil nil nil)))
(if (not (file-exists-p file-name))
(throw 'file-name file-name)
(when (y-or-n-p
(format "File `%s' exists; overwrite? " file-name))
(throw 'file-name file-name)))))))
(output-buffer (buffer-substring-no-properties
(point-min) (point-max))))
(with-temp-buffer
(set (make-local-variable 'require-final-newline) t)
(insert output-buffer)
;; Ensure there's a final newline
(and (> (point-max) (point-min))
(not (= (char-after (1- (point-max))) ?\n))
(insert "\n"))
(write-region
(progn
;; Remove possible logs from output.
(goto-char (point-min))
(re-search-forward
"^\\[\\|^<\\?xml +version=\"\\|^- +fields: " nil t)
(beginning-of-line 1)
(point))
(point-max)
file-name))
(kill-buffer)
(python-django-mgmt-restore-window-configuration)
(message "Fixture saved to file `%s'." file-name))))
(python-django-qmgmt-define dumpdata-all
"Save the contents of the database as a fixture for all apps."
(:submenu "Database" :binding "ddp" :no-pop t :capture-output t
:functions #'python-django-qmgmt-dumpdata-function)
(database (python-django-minibuffer-read-database "Database: " default)
"default" "--database=")
(indent (number-to-string
(read-number "Indent Level: "
(string-to-number default)))
(number-to-string python-django-qmgmt-dumpdata-default-indent)
"--indent=")
(format (python-django-minibuffer-read-from-list
"Dump to format: " python-django-qmgmt-dumpdata-formats default)
"json" "--format="))
(python-django-qmgmt-define dumpdata-app
"Save the contents of the database as a fixture for the specified app."
(:submenu "Database" :binding "dda" :no-pop t :capture-output t
:functions #'python-django-qmgmt-dumpdata-function)
(database (python-django-minibuffer-read-database "Database: " default)
"default" "--database=")
(indent (number-to-string
(read-number "Indent Level: "
(string-to-number default))) "4" "--indent=")
(format (python-django-minibuffer-read-from-list
"Dump to format: " python-django-qmgmt-dumpdata-formats default)
"json" "--format=")
(app (python-django-minibuffer-read-app "Dumpdata for App: ")))
(python-django-qmgmt-define flush
"Execute 'sqlflush' on the given database."
(:submenu "Database" :binding "df" :msg "Flushed database"
:functions #'python-django-qmgmt-kill-and-msg-function)
(database (python-django-minibuffer-read-database "Database: " default)
"default" "--database="))
(python-django-qmgmt-define loaddata
"Install the named fixture(s) in the database."
(:submenu "Database" :binding "dl"
:functions #'python-django-qmgmt-kill-and-msg-function)
(database (python-django-minibuffer-read-database "Database: " default)
"default" "--database=")
(fixtures (python-django-minibuffer-read-file-names "Fixtures: ")))
(python-django-qmgmt-define validate
"Validate all installed models."
(:submenu "Database" :binding "dv"
:functions #'python-django-qmgmt-kill-and-msg-function))
(defun python-django-qmgmt-graph_models-function (status-ok args)
"Callback for graph_model quick management command.
Argument STATUS-OK is non-nil if process exited successfully.
Argument ARGS is a plist with the switches and arguments passed
to the command. See `python-django-qmgmt-define' docstring for
details."
(when status-ok
(let ((open (y-or-n-p "Open generated graph? ")))
(kill-buffer)
(python-django-mgmt-restore-window-configuration)
(when open
(find-file (plist-get args 'filename))))))
(python-django-qmgmt-define graph_models-all
"Creates a Graph of models for all project apps."
(:submenu "Database" :switches "-ag" :binding "dgg"
:functions #'python-django-qmgmt-graph_models-function)
(filename
(expand-file-name
(read-file-name "Filename for generated Graph: "
default default))
(expand-file-name
"graph_all.png" python-django-project-root)
"--output=" t))
(python-django-qmgmt-define graph_models-apps
"Creates a Graph of models for given apps."
(:submenu "Database" :binding "dga"
:functions #'python-django-qmgmt-graph_models-function)
(apps (python-django-minibuffer-read-apps "Graph for App: "))
(filename
(expand-file-name
(read-file-name "Filename for generated Graph: " default default))
(expand-file-name
(format "graph_%s.png" (replace-regexp-in-string " " "_" apps))
python-django-project-root)
"--output=" t))
;; i18n
(python-django-qmgmt-define makemessages-all
"Create/Update translation string files."
(:submenu "i18n" :switches "--all" :binding "im"
:functions #'python-django-qmgmt-kill-and-msg-function))
(python-django-qmgmt-define compilemessages-all
"Compile project .po files to .mo."
(:submenu "i18n" :binding "ic"
:functions #'python-django-qmgmt-kill-and-msg-function))
;; Dev Server
(defcustom python-django-qmgmt-runserver-default-bindaddr "localhost:8000"
"Default binding address for quick runserver."
:group 'python-django
:type 'string
:safe 'stringp)
(defcustom python-django-qmgmt-testserver-default-bindaddr "localhost:8000"
"Default binding address for quick testserver."
:group 'python-django
:type 'string
:safe 'stringp)
(defcustom python-django-qmgmt-mail_debug-default-bindaddr "localhost:1025"
"Default binding address for quick mail_debug."
:group 'python-django
:type 'string
:safe 'stringp)
(python-django-qmgmt-define runserver
"Start development Web server."
(:submenu "Server" :binding "rr"
:make-comint-function #'python-django-mgmt-make-comint-for-runserver)
(bindaddr "Serve on [ip]:[port]: "
python-django-qmgmt-runserver-default-bindaddr))
(python-django-qmgmt-define runserver_plus
"Start extended development Web server."
(:submenu "Server" :binding "rp"
:make-comint-function #'python-django-mgmt-make-comint-for-runserver)
(bindaddr "Serve on [ip]:[port]: "
python-django-qmgmt-runserver-default-bindaddr))
(python-django-qmgmt-define testserver
"Start development server with data from the given fixture(s)."
(:submenu "Server" :binding "rt")
(bindaddr "Serve on [ip]:[port]: "
python-django-qmgmt-testserver-default-bindaddr)
(fixtures (python-django-minibuffer-read-file-names "Fixtures: ")))
(python-django-qmgmt-define mail_debug
"Start a test mail server for development."
(:submenu "Server" :binding "rm")
(bindaddr "Serve on [ip]:[port]: "
python-django-qmgmt-mail_debug-default-bindaddr))
;; Testing
(python-django-qmgmt-define test-all
"Run the test suite for the entire project."
(:submenu "Test" :binding "tp"
:functions #'python-django-qmgmt-kill-and-msg-function))
(python-django-qmgmt-define test-app
"Run the test suite for the specified app."
(:submenu "Test" :binding "ta"
:functions #'python-django-qmgmt-kill-and-msg-function)
(app (python-django-minibuffer-read-app "Test App: ")))
;; South integration
(defun python-django-qmgmt-open-migration-function (status-ok args)
"Callback for commands that create migrations.
Argument STATUS-OK is non-nil if process exited successfully.
Argument ARGS is a plist with the switches and arguments passed
to the command. See `python-django-qmgmt-define' docstring for
details."
(when status-ok
(let ((app (cdr (assq 'app args))))
(python-django-qmgmt-kill-and-msg-function status-ok args)
(and (y-or-n-p "Open the created migration? ")
(find-file
(expand-file-name
(car (last (python-django-info-get-app-migrations app)))
(expand-file-name
"migrations"
(python-django-info-get-app-path app))))))))
(python-django-qmgmt-define convert_to_south
"Convert given app to South."
(:submenu "South" :binding "soc"
:functions #'python-django-qmgmt-kill-and-msg-function)
(app (python-django-minibuffer-read-app "Convert App: ")))
(python-django-qmgmt-define datamigration
"Create a new datamigration for the given app."
(:submenu "South" :binding "sod"
:functions #'python-django-qmgmt-open-migration-function)
(app (python-django-minibuffer-read-app "Datamigration for App: "))
(name "Datamigration name: "))
(python-django-qmgmt-define migrate-all
"Run all migrations for all apps."
(:submenu "South" :switches "--all" :binding "somp"
:functions #'python-django-qmgmt-kill-and-msg-function)
(database (python-django-minibuffer-read-database "Database: " default)
"default" "--database="))
(python-django-qmgmt-define migrate-app
"Run all migrations for given app."
(:submenu "South" :binding "soma"
:functions #'python-django-qmgmt-kill-and-msg-function)
(database (python-django-minibuffer-read-database "Database: " default)
"default" "--database=")
(app (python-django-minibuffer-read-app "Migrate App: ")))
(python-django-qmgmt-define migrate-list
"Run all migrations for all apps."
(:submenu "South" :switches "--list" :binding "soml"
:functions #'python-django-qmgmt-kill-and-msg-function)
(database (python-django-minibuffer-read-database "Database: " default)
"default" "--database="))
(python-django-qmgmt-define migrate-app-to
"Run migrations for given app [up|down]-to given number."
(:submenu "South" :binding "somt"
:functions #'python-django-qmgmt-kill-and-msg-function)
(database (python-django-minibuffer-read-database "Database: " default)
"default" "--database=")
(app (python-django-minibuffer-read-app "Migrate App: "))
(migration (python-django-minibuffer-read-migration "To migration: " app)))
(python-django-qmgmt-define schemamigration-initial
"Create the initial schemamigration for the given app."
(:submenu "South" :switches "--initial" :binding "sosi"
:functions #'python-django-qmgmt-kill-and-msg-function)
(app (python-django-minibuffer-read-app
"Initial schemamigration for App: ")))
(python-django-qmgmt-define schemamigration
"Create new empty schemamigration for the given app."
(:submenu "South" :switches "--empty" :binding "soss"
:functions #'python-django-qmgmt-open-migration-function)
(app (python-django-minibuffer-read-app
"Initial schemamigration for App: "))
(name "Schemamigration name: "))
(python-django-qmgmt-define schemamigration-auto
"Create an automatic schemamigration for the given app."
(:submenu "South" :switches "--auto" :binding "sosa"
:functions #'python-django-qmgmt-open-migration-function)
(app (python-django-minibuffer-read-app
"Auto schemamigration for App: ")))
;;; Fast commands
(defcustom python-django-cmd-etags-command
"etags `find -name \"*.py\"`"
"Command used to build tags tables."
:group 'python-django
:type 'string)
(defcustom python-django-cmd-grep-function nil
"Function to grep on a directory.
The function receives no args, however `default-directory' will
default to a sane value."
:group 'python-django
:type 'function)
(defun python-django-cmd-build-etags ()
"Build tags for current project."
(interactive)
(let ((current-dir default-directory))
(cd
(file-name-directory
python-django-project-manage.py))
(if (eq 0
(shell-command
python-django-cmd-etags-command))
(message "Tags created sucessfully")
(message "Tags creation failed"))
(cd current-dir)))
(defun python-django-cmd-grep ()
"Grep in project directories."
(interactive)
(let ((default-directory
(or (python-django-ui-directory-at-point)
(file-name-directory
python-django-project-manage.py))))
(if (not python-django-cmd-grep-function)
(call-interactively #'rgrep)
(funcall
python-django-cmd-grep-function default-directory))))
(defun python-django-cmd-open-docs ()
"Open Django documentation in a browser."
(interactive)
(browse-url
(format
"https://docs.djangoproject.com/en/%s/"
(substring (python-django-info-get-version) 0 3))))
(defun python-django-cmd-visit-settings ()
"Visit settings file."
(interactive)
(find-file (python-django-info-module-path
python-django-project-settings)))
(defun python-django-cmd-visit-virtualenv ()
"Visit virtualenv directory."
(interactive)
(and python-shell-virtualenv-path
(dired python-shell-virtualenv-path)))
(defun python-django-cmd-visit-project-root ()
"Visit project root directory."
(interactive)
(dired python-django-project-root))
(defun python-django-cmd-dired-at-point ()
"Open dired at current tree node."
(interactive)
(let ((dir (python-django-ui-directory-at-point)))
(and dir (dired dir))))
(defun python-django-cmd-directory-at-point ()
"Message the current directory at point."
(interactive)
(message (or (python-django-ui-directory-at-point) "")))
(defun python-django-cmd-jump-to-app (app)
"Jump to APP's directory."
(interactive
(list
(python-django-minibuffer-read-app "Jump to app: ")))
(when (python-django-info-get-app-path app)
(goto-char (point-min))
(re-search-forward (format " %s" app))
(python-django-ui-move-to-closest-icon)))
(defun python-django-cmd-jump-to-media (which)
"Jump to a WHICH media directory."
(interactive
(list
(python-django-minibuffer-read-from-list
"Jump to: " '("MEDIA_ROOT" "STATIC_ROOT"))))
(goto-char (point-min))
(re-search-forward (format " %s" which))
(python-django-ui-move-to-closest-icon))
(defun python-django-cmd-jump-to-template-dir (which)
"Jump to a WHICH template directory."
(interactive
(list
(python-django-minibuffer-read-from-list
"Jump to: "
(mapcar 'identity (python-django-info-get-setting "TEMPLATE_DIRS")))))
(goto-char (point-min))
(re-search-forward (format " %s" which))
(python-django-ui-move-to-closest-icon))
;;; UI stuff
(defvar python-django-ui-ignored-dirs
'("." ".." ".bzr" ".cdv" "~.dep" "~.dot" "~.nib" "~.plst" ".git" ".hg" ".pc"
".svn" "_MTN" "blib" "CVS" "RCS" "SCCS" "_darcs" "_sgbak" "autom4te.cache"
"cover_db" "_build" ".ropeproject" "__pycache__")
"Directories ignored when scanning project files.")
(defvar python-django-ui-allowed-extensions
'("css" "gif" "htm" "html" "jpg" "js" "json" "mo" "png" "po" "py" "txt" "xml"
"yaml" "scss" "less")
"Allowed extensions when scanning project files.")
(defcustom python-django-ui-image-enable t
"Enable images for widgets?"
:group 'python-django
:type 'boolean
:safe 'booleanp)
(defcustom python-django-ui-theme "folder"
"Default theme for widgets."
:group 'python-django
:type 'boolean
:safe 'stringp)
(defcustom python-django-ui-buffer-switch-function 'switch-to-buffer
"Function for switching to the project buffer.
The function receives one argument, the status buffer."
:group 'python-django
:type '(radio (function-item switch-to-buffer)
(function-item pop-to-buffer)
(function-item display-buffer)
(function :tag "Other")))
(defun python-django-ui-show-buffer (buffer)
"Show the Project BUFFER."
(funcall python-django-ui-buffer-switch-function buffer))
(defun python-django-ui-clean ()
"Empty current UI buffer."
(let ((inhibit-read-only t))
(erase-buffer)))
(defun python-django-ui-insert-header ()
"Draw header information."
(insert
(format "%s\t\t%s\n"
(propertize
"Django Version:"
'face 'python-django-face-title)
(propertize
(python-django-info-get-version)
'face 'python-django-face-django-version))
(format "%s\t\t%s\n"
(propertize
"Project:"
'face 'python-django-face-title)
(propertize
python-django-project-root
'face 'python-django-face-project-root))
(format "%s\t\t%s\n"
(propertize
"Settings:"
'face 'python-django-face-title)
(propertize
python-django-project-settings
'face 'python-django-face-settings-module))
(format "%s\t\t%s"
(propertize
"Virtualenv:"
'face 'python-django-face-title)
(propertize
(or python-shell-virtualenv-path "None")
'face 'python-django-face-virtualenv-path))
"\n\n\n"))
(defun python-django-ui-build-section-alist ()
"Create section Alist for current project."
(list
(cons
"Apps"
(mapcar
(lambda (app)
(cons app (python-django-info-get-app-path app)))
(python-django-info-get-installed-apps)))
(cons
"Media"
(list
(cons "MEDIA_ROOT" (python-django-info-get-setting "MEDIA_ROOT"))
(cons "STATIC_ROOT" (python-django-info-get-setting "STATIC_ROOT"))))
(cons
"Static Content" (mapcar
(lambda (dir)
;; STATICFILES_DIRS elements can be either a
;; string or a size-two tuple with the first
;; element being the prefix and the latter
;; being the path: http://bit.ly/16Fw9xW
(if (stringp dir)
(cons dir dir)
(cons (aref dir 0) (aref dir 1))))
(python-django-info-get-setting "STATICFILES_DIRS")))
(cons
"Templates" (mapcar
(lambda (dir)
(cons dir dir))
(python-django-info-get-setting "TEMPLATE_DIRS")))))
;; Many kudos to Ye Wenbin since dirtree.el was of great help when
;; looking for examples of `tree-widget':
;; https://github.com/zkim/emacs-dirtree/blob/master/
(define-widget 'python-django-ui-tree-section-widget 'tree-widget
"Tree widget for sections of Django Project buffer."
:expander 'python-django-ui-tree-section-widget-expand
:help-echo 'ignore
:has-children t)
(define-widget 'python-django-ui-tree-section-node-widget 'push-button
"Widget for a nodes of `python-django-ui-tree-section-widget'."
:format "%[%t%]\n"
:button-face 'default
:notify 'python-django-ui-tree-section-widget-expand)
(define-widget 'python-django-ui-tree-dir-widget 'tree-widget
"Tree widget for directories of Django Project."
:expander 'python-django-ui-tree-dir-widget-expand
:help-echo 'ignore
:has-children t)
(define-widget 'python-django-ui-tree-file-widget 'push-button
"Widget for a files inside the `python-django-ui-tree-dir-widget'."
:format "%[%t%]\n"
:button-face 'default
:notify 'python-django-ui-tree-file-widget-select)
(defun python-django-ui-tree-section-widget-expand (tree &rest ignore)
"Expand directory for given section TREE widget.
Optional argument IGNORE is there for compatibility."
(or (widget-get tree :args)
(let ((section-alist (widget-get tree :section-alist)))
(mapcar (lambda (section)
(let ((name (car section))
(dir (cdr section)))
`(python-django-ui-tree-dir-widget
:node (python-django-ui-tree-file-widget
:tag ,name
:file ,dir)
:file ,dir
:open nil
:indent 0)))
section-alist))))
(defun python-django-ui-tree-dir-widget-expand (tree)
"Expand directory for given TREE widget."
(or (widget-get tree :args)
(let* ((dir (widget-get tree :file))
dir-list file-list)
(when (and dir (file-exists-p dir))
(dolist (file (directory-files dir t))
(let ((basename (file-name-nondirectory file)))
(if (file-directory-p file)
(when (not (member basename python-django-ui-ignored-dirs))
(setq dir-list (cons basename dir-list)))
(when (member (python-django-util-file-name-extension file)
python-django-ui-allowed-extensions)
(setq file-list (cons basename file-list))))))
(setq dir-list (sort dir-list 'string<))
(setq file-list (sort file-list 'string<))
(append
(mapcar (lambda (file)
`(python-django-ui-tree-dir-widget
:file ,(expand-file-name file dir)
:node (python-django-ui-tree-file-widget
:tag ,file
:file ,(expand-file-name file dir))))
dir-list)
(mapcar (lambda (file)
`(python-django-ui-tree-file-widget
:file ,(and file (not (string= file ""))
(expand-file-name file dir))
:tag ,file))
file-list))))))
(defun python-django-ui-tree-file-widget-select (node &rest ignore)
"Open file in other window.
Argument NODE and IGNORE are just for compatibility."
(let ((file (widget-get node :file)))
(and file (find-file-other-window file))))
(defun python-django-ui-tree-section-insert (name section-alist)
"Create tree widget for NAME and SECTION-ALIST."
(apply 'widget-create
`(python-django-ui-tree-section-widget
:node (python-django-ui-tree-section-node-widget
:tag ,name)
:section-alist ,section-alist
:open t)))
(defun python-django-ui-widget-move (arg)
"Move between widgets sensibly in the project buffer.
Movement between widgets of the tree happen line by line, leaving
point next to the closest icon available. With positive ARG move
forward that many times, else backwards."
(let* ((success-moves 0)
(forward (> arg 0))
(func (if forward
'widget-forward
'widget-backward))
(abs-arg (abs arg)))
(catch 'nowidget
(while (> abs-arg success-moves)
(if (memq (widget-type (widget-at (point)))
'(tree-widget-close-icon
tree-widget-empty-icon
tree-widget-leaf-icon
tree-widget-open-icon))
(ignore-errors (funcall func 2))
(ignore-errors (funcall func 1)))
(when (not (widget-at (point)))
(throw 'nowidget t))
(setq success-moves (1+ success-moves))))
(python-django-ui-move-to-closest-icon)
(setq default-directory
(or (python-django-ui-directory-at-point)
(file-name-directory python-django-project-manage.py)))))
(defun python-django-ui-widget-forward (arg)
"Move point to the next line's main widget.
With optional ARG, move across that many fields."
(interactive "p")
(python-django-ui-widget-move arg))
(defun python-django-ui-widget-backward (arg)
"Move point to the previous line's main widget.
With optional ARG, move across that many fields."
(interactive "p")
(python-django-ui-widget-move (- arg)))
(defun python-django-ui-move-up-tree (arg)
"Move point to the parent widget of the tree.
With optional ARG, move across that many fields."
(interactive "p")
(and (< arg 0) (setq arg (- arg)))
(python-django-ui-move-to-closest-icon)
(let ((start-depth (- (point) (line-beginning-position))))
(when (not (= 0 start-depth))
(while (<= start-depth (- (point) (line-beginning-position)))
(python-django-ui-widget-backward 1)))))
(defun python-django-ui-beginning-of-widgets ()
"Move to the first widget.
With optional ARG, move across that many fields."
(interactive)
(goto-char (point-min))
(python-django-ui-widget-forward 1))
(defun python-django-ui-end-of-widgets ()
"Move point to last widget.
With optional ARG, move across that many fields."
(interactive)
(goto-char (point-max))
(python-django-ui-widget-backward 1))
(defun python-django-ui-move-to-closest-icon ()
"Move to closest button from point."
(interactive)
(if (and
(not (widget-at (point)))
(not (widget-at (1- (point)))))
(progn
(widget-backward 1)
(beginning-of-line 1)
(widget-forward 1))
(beginning-of-line 1)
(and (not (widget-at (point)))
(widget-forward 1))))
(defun python-django-ui-safe-button-press ()
"Move to closest button from point and press it."
(interactive)
(and (not (widget-at (point)))
(python-django-ui-move-to-closest-icon))
(widget-button-press (point)))
(defun python-django-ui-widget-type-at-point ()
"Return the node type for current position."
(let* ((widget (widget-at (point)))
(file-p (widget-get
(tree-widget-node widget)
:tree-widget--guide-flags)))
(and widget (if file-p 'file 'dir))))
(defun python-django-ui-directory-at-point ()
"Return the node type for current position."
(widget-get
(widget-get (tree-widget-node (widget-at (point))) :parent) :file))
;;;Main functions
(defcustom python-django-known-projects nil
"Alist of known projects."
:group 'python-django
:type '(repeat (list string string string))
:safe (lambda (val)
(and
(stringp (car val))
(stringp (nth 1 val))
(stringp (nth 2 val)))))
(defun python-django-mode-find-next-buffer ()
"Find the next Django project buffer available."
(let ((current-buffer (current-buffer)))
(catch 'buffer
(dolist (buf (buffer-list))
(and (with-current-buffer buf
(and (eq major-mode 'python-django-mode)
(not (equal buf current-buffer))))
(throw 'buffer buf))))))
(defun python-django-mode-on-kill-buffer ()
"Hook run on `buffer-kill-hook'."
(and (python-django-mgmt-buffer-list (current-buffer))
(call-interactively 'python-django-mgmt-kill-all)))
(define-derived-mode python-django-mode special-mode "Django"
"Major mode to manage Django projects.
\\{python-django-mode-map}")
;;;###autoload
(defun python-django-open-project (directory settings &optional existing)
"Open a Django project at given DIRECTORY using SETTINGS.
Optional argument EXISTING is internal and should not be used.
The recommended way to chose your project root, is to use the
directory containing your settings module; for instance if your
settings module is in /path/django/settings.py, use /path/django/
as your project path and django.settings as your settings module.
When called with no `prefix-arg', this function will try to find
an opened project-buffer, if current buffer is already a project
buffer it will cycle to next opened project. If no project
buffers are found, then the user prompted for the project path
and settings module unless `python-django-project-root' and
`python-django-project-settings' are somehow set, normally via
directory local variables. If none of the above matched or the
function is called with one `prefix-arg' and there are projects
defined in the `python-django-known-projects' variable the user
is prompted for any of those known projects, if the variable
turns to be nil the user will be prompted for project-path and
settings module (the same happens when called with two or more
`prefix-arg')."
(interactive
(let ((buf
;; Get an existing project buffer that's not the current.
(python-django-mode-find-next-buffer)))
(cond
((and (not current-prefix-arg)
(not buf)
python-django-project-root
python-django-project-settings)
;; There's no existing buffer but project variables are
;; set, so use them to open the project.
(list python-django-project-root
python-django-project-settings
;; if the user happens to be in the project buffer
;; itself, do nothing.
(and (eq major-mode 'python-django-mode)
(current-buffer))))
((and (not current-prefix-arg) buf)
;; there's an existing buffer move/cycle to it.
(with-current-buffer buf
(list
python-django-project-root
python-django-project-settings
buf)))
((or (and python-django-known-projects
(<= (prefix-numeric-value current-prefix-arg) 4)))
;; When there are known projects and called at most with one
;; prefix arg try opening a known project.
(cdr
(assoc
(python-django-minibuffer-read-from-list
"Project: " python-django-known-projects)
python-django-known-projects)))
(t
(let ((root))
;; When called with two or more prefix arguments or all
;; input methods failed.
(list
(setq root (read-directory-name
"Project Root: " python-django-project-root nil t))
(read-string
"Settings module: "
(or python-django-project-settings
(format "%s.settings"
(python-django-info-directory-basename root))))))))))
(if (not existing)
(let* ((directory (expand-file-name directory))
(project-name (python-django-info-directory-basename directory))
(buffer-name
(format "*Django: %s (%s)*"
project-name
(python-django-util-shorten-settings settings)))
(success t))
(with-current-buffer (get-buffer-create buffer-name)
(let ((inhibit-read-only t))
(python-django-mode)
(python-django-ui-clean)
(set (make-local-variable
'python-django-info--get-setting-cache) nil)
(set (make-local-variable
'python-django-info--get-version-cache) nil)
(set (make-local-variable
'python-django-info--get-app-paths-cache) nil)
(set (make-local-variable
'python-django-project-root) directory)
(set (make-local-variable
'python-django-project-settings) settings)
(set (make-local-variable
'python-django-project-name) project-name)
(set (make-local-variable
'python-django-project-manage.py)
(python-django-info-find-manage.py directory))
(set (make-local-variable 'default-directory)
(file-name-directory
python-django-project-manage.py))
(python-django-util-clone-local-variables)
(set (make-local-variable 'tree-widget-image-enable)
python-django-ui-image-enable)
(tree-widget-set-theme python-django-ui-theme)
(condition-case err
(progn
(python-django-ui-insert-header)
(mapc (lambda (section)
(python-django-ui-tree-section-insert
(car section) (cdr section))
(insert "\n"))
(python-django-ui-build-section-alist)))
(user-error
(setq success nil)
(insert (error-message-string err))
(goto-char (point-min)))))
(when success
(add-hook 'kill-buffer-hook
#'python-django-mode-on-kill-buffer nil t)
(python-django-ui-beginning-of-widgets))
(python-django-ui-show-buffer (current-buffer))))
(python-django-ui-show-buffer existing)))
;; Stolen from magit.
(defun python-django-close-project (&optional kill-buffer)
"Bury the buffer and delete its window.
With a prefix argument, KILL-BUFFER instead."
(interactive "P")
(quit-window kill-buffer (selected-window)))
(defun python-django-refresh-project ()
"Refresh Django project."
(interactive)
(python-django-open-project
python-django-project-root
python-django-project-settings))
(provide 'python-django)
;; Local Variables:
;; coding: utf-8
;; indent-tabs-mode: nil
;; End:
;;; python-django.el ends here