;;; python-django.el --- A Jazzy package for managing Django projects ;; Copyright (C) 2011 Free Software Foundation, Inc. ;; Author: Fabián E. Gallina ;; URL: https://github.com/fgallina/python-django.el ;; 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 ;; . ;;; 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 (format "%s %s help%s" (executable-find python-shell-interpreter) python-django-project-manage.py (or (and command (concat " " 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 (concat "app_paths = {}\n" "for app in settings.INSTALLED_APPS:\n" " mod = __import__(app)\n" " if '.' in app:\n" " for sub in app.split('.')[1:]:\n" " mod = getattr(mod, sub)\n" " app_paths[app] = dirname(abspath(mod.__file__))\n" "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-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 full) "Read django app from minibuffer. PROMPT is a string to prompt user for app. Optional argument INITIAL-INPUT is the initial prompted value. When FULL is non-nill the full module name for the installed app is prompted." (let ((apps (mapcar (lambda (app) (if (not full) (car (last (split-string app "\\."))) app)) (python-django-info-get-setting "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 full) "Read django apps from minibuffer. PROMPT is a string to prompt user for app. Optional argument INITIAL-INPUT is the initial prompted value. When FULL is non-nill the full module name for the installed app is prompted." (python-django-minibuffer-read-list 'python-django-minibuffer-read-app prompt full)) (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))) (set (make-local-variable 'python-django-mgmt--available-commands) (let ((help-string (python-django-help)) (commands)) (with-temp-buffer (insert help-string) (goto-char (point-min)) (delete-region (and (re-search-forward "Usage: manage.py") (line-beginning-position)) (or (and (re-search-forward "Available subcommands:\n" nil t) (forward-line -1) (line-beginning-position)) (point-max))) (goto-char (point-min)) (re-search-forward "Available subcommands:\n") (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-shell_plus (command process-name) "Run COMMAND with PROCESS-NAME in generic Comint buffer." (python-django-mgmt-make-comint-for-shell 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-runserver_plus (command process-name) "Run COMMAND with PROCESS-NAME in generic Comint buffer." (python-django-mgmt-make-comint-for-runserver 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) "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." (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-special-func-name (intern (format "python-django-mgmt-make-comint-for-%s" command))) (full-command (format "%s %s %s" python-django-project-manage.py command args))) (if (not (fboundp make-comint-special-func-name)) (python-django-mgmt-make-comint full-command process-name) (funcall make-comint-special-func-name 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 (defvar python-django-qmgmt-process-status-ok nil "Non-nil if management command process ended successfully. This variable is set automatically and locally to the management command process buffer after the process finishes, making it available for user defined callbacks. See `python-django-qmgmt-kill-and-msg-callback' as an example for how to use this variable to execute callback code only if process ended successfully.") (defmacro python-django-qmgmt-define (name doc-or-args &optional args &rest iswitches) "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 ISWITCHES 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-callback' 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 menu is added to the root. + :switches, when defined, the new command is executed with these fixed switches. 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. ISWITCHES have the form (VARNAME PROMPT DEFAULT SWITCH FORCE-ASK), you can add 0 or more ISWITCHES 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 ISWITCHES and ARGS with the additional :command key holding the executed command. See the `python-django-qmgmt-kill-and-msg-callback' function for a nice example of a callback." (declare (indent defun)) (let* ((docstring (and (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 iswitches and ;; set args to nil. (setq iswitches (cons args iswitches)) nil)) (defun-name (intern (format "qmgmt-%s" name))) (full-name (intern (format "python-django-%s" defun-name))) (callback (intern (format "%s-callback" full-name))) (command (car (split-string (format "%s" name) "-"))) (binding (plist-get args :binding)) (capture-output (plist-get args :capture-output)) (no-pop (plist-get args :no-pop)) (quick-submenu (plist-get args :submenu)) (switches (plist-get args :switches)) (keys (and binding (format "c%s" binding))) (defargs (mapcar 'car iswitches)) (cmd-spec ;; The spec is the shell command with placeholders in it. ;; Example: ./manage.py dumpdata --database= ;; --indent= --format= (concat (format "./manage.py %s " command) (and switches (format "%s " switches)) (and defargs (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))) iswitches " ")))) (interactive-code (mapcar (lambda (arg) (let* ((default (nth 2 arg)) (switch (nth 3 arg)) (switch (cond ((eq (length switch) 0) "") ((eq ?= (car (last (append switch nil)))) switch) (t (format "%s " switch)))) (force-ask (nth 4 arg)) (read-func (if (listp (nth 1 arg)) (nth 1 arg) `(read-string ,(nth 1 arg) default)))) `(concat ,switch (setq ,(car arg) (let ((default ,default)) (if (or ,force-ask current-prefix-arg (not default)) ,read-func default)))))) iswitches)) (item-docstring (if docstring (car (split-string docstring "\n")) (format "Run ./manage.py %s" cmd-spec))) (full-docstring (format "%s\n\n%s\n\n%s" cmd-spec (or docstring item-docstring) (concat "This is an interactive command defined by " "`python-django-qmgmt-define' macro.\n" "Users can override any parameter with defaults by " "calling this command with `prefix-arg' .\n\n" (and switches (format "Default switches: \n\n * %s\n\n" switches)) (and iswitches (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"))))) iswitches "\n\n"))))))) `(progn (defun ,full-name ,defargs ,full-docstring (interactive (let ,defargs (list ,@interactive-code))) (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)))) (message "Running: ./manage.py %s %s" ,command cmd-args) (when (fboundp ',callback) ;; There's a callback defined, we create another function ;; by applying partially the existing callback and giving ;; it an alist with all the user selected values as an ;; argument. Then the callback is executed when the ;; process finishes correctly. (set-process-sentinel process (apply-partially #'(lambda (cb process status) (set-buffer (process-buffer process)) (set (make-local-variable 'python-django-qmgmt-process-status-ok) (string= status "finished\n")) (funcall cb)) (apply-partially ',callback (append (cons ;; Add the executed command. (cons :command ,command) ;; Convert the args plist to an alist (mapcar (lambda (key) (cons key (plist-get ',args key))) (let ((lst) (i 0)) ;; Collect all keys from args plist (while (< i (length ',args)) (setq lst (cons (nth i ',args) lst)) (setq i (+ i 2))) lst))) ;; Retrieve all switches. (mapcar #'(lambda (sym) (let ((val (symbol-value sym))) (cons sym (cond ((string-match "^--" val) (substring val (1+ (string-match "=" val)))) ((string-match "^-" val) (substring val 3)) (t val))))) ',defargs) nil))))))) ;; Add the specified binding for this quick command. (and ,binding (ignore-errors (define-key python-django-mode-map ,keys ',full-name))) ;; Add menu stuff. (easy-menu-add-item 'python-django-menu nil (list ,quick-submenu) "---") (easy-menu-add-item 'python-django-menu (list ,quick-submenu) [,cmd-spec ,full-name :help ,item-docstring :active (member ,command (python-django-mgmt-list-commands)) ] "---") ;; Just like defun, return the defined function. #',full-name))) (defun python-django-qmgmt-kill-and-msg-callback (args) "Kill the process buffer and show message or output. Argument ARGS is an alist with the arguments passed to the management command." (when python-django-qmgmt-process-status-ok (let ((msg (or (cdr (assq :msg args)) (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")) (defalias 'python-django-qmgmt-collectstatic-callback 'python-django-qmgmt-kill-and-msg-callback) (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.")) (defalias 'python-django-qmgmt-clean_pyc-callback 'python-django-qmgmt-kill-and-msg-callback) (python-django-qmgmt-define create_command "Create management commands directory structure for app." (:submenu "Tools" :binding "occ" :no-pop t) (app (python-django-minibuffer-read-app "App name: "))) (defun python-django-qmgmt-create_command-callback (args) "Callback for create_command quick management command. Optional argument ARGS args for it." (when python-django-qmgmt-process-status-ok (let* ((appname (cdr (assoc 'app args))) (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"))))) (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 startapp "Create new Django app for current project." (:submenu "Tools" :binding "osa" :no-pop t) (app "App name: ")) (defun python-django-qmgmt-startapp-callback (args) "Callback for clean_pyc quick management command. Optional argument ARGS args for it." (when python-django-qmgmt-process-status-ok (let ((appname (cdr (assoc 'app args))) (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))) ;; Shell (python-django-qmgmt-define shell "Run a Python interpreter for this project." (:submenu "Shell" :binding "ss")) (python-django-qmgmt-define shell_plus "Like the 'shell' but autoloads all models." (:submenu "Shell" :binding "sp")) ;; Database (python-django-qmgmt-define syncdb "Sync database tables for all INSTALLED_APPS." (:submenu "Database" :binding "dsy" :no-pop t) (database (python-django-minibuffer-read-database "Database: " default) "default" "--database=")) (defalias 'python-django-qmgmt-syncdb-callback 'python-django-qmgmt-kill-and-msg-callback) (python-django-qmgmt-define dbshell "Run the command-line client for specified database." (:submenu "Database" :binding "dsh") (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) (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) (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) (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: "))) (defun python-django-qmgmt-dumpdata-callback (args) "Callback executed after dumpdata finishes. ARGS is an alist containing arguments passed to the quick management command." (when python-django-qmgmt-process-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)))) (defalias 'python-django-qmgmt-dumpdata-app-callback 'python-django-qmgmt-dumpdata-callback) (defalias 'python-django-qmgmt-dumpdata-all-callback 'python-django-qmgmt-dumpdata-callback) (python-django-qmgmt-define flush "Execute 'sqlflush' on the given database." (:submenu "Database" :binding "df" :msg "Flushed database") (database (python-django-minibuffer-read-database "Database: " default) "default" "--database=")) (defalias 'python-django-qmgmt-flush-callback 'python-django-qmgmt-kill-and-msg-callback) (python-django-qmgmt-define loaddata "Install the named fixture(s) in the database." (:submenu "Database" :binding "dl") (database (python-django-minibuffer-read-database "Database: " default) "default" "--database=") (fixtures (python-django-minibuffer-read-file-names "Fixtures: "))) (defalias 'python-django-qmgmt-loaddata-callback 'python-django-qmgmt-kill-and-msg-callback) (python-django-qmgmt-define validate "Validate all installed models." (:submenu "Database" :binding "dv")) (defalias 'python-django-qmgmt-validate-callback 'python-django-qmgmt-kill-and-msg-callback) (python-django-qmgmt-define graph_models-all "Creates a Graph of models for all project apps." (:submenu "Database" :switches "-ag" :binding "dgg") (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") (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)) (defun python-django-qmgmt-graph_models-callback (args) "Callback for graph_model quick management command. Optional argument ARGS args for it." (when python-django-qmgmt-process-status-ok (let ((open (y-or-n-p "Open generated graph? "))) (kill-buffer) (python-django-mgmt-restore-window-configuration) (and open (find-file (cdr (assoc 'filename args))))))) (defalias 'python-django-qmgmt-graph_models-all-callback 'python-django-qmgmt-graph_models-callback) (defalias 'python-django-qmgmt-graph_models-apps-callback 'python-django-qmgmt-graph_models-callback) ;; i18n (python-django-qmgmt-define makemessages-all "Create/Update translation string files." (:submenu "i18n" :switches "--all" :binding "im")) (defalias 'python-django-qmgmt-makemessages-all-callback 'python-django-qmgmt-kill-and-msg-callback) (python-django-qmgmt-define compilemessages-all "Compile project .po files to .mo." (:submenu "i18n" :binding "ic")) (defalias 'python-django-qmgmt-compilemessages-all-callback 'python-django-qmgmt-kill-and-msg-callback) ;; 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") (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") (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")) (defalias 'python-django-qmgmt-test-all-callback 'python-django-qmgmt-kill-and-msg-callback) (python-django-qmgmt-define test-app "Run the test suite for the specified app." (:submenu "Test" :binding "ta") (app (python-django-minibuffer-read-app "Test App: "))) (defalias 'python-django-qmgmt-test-app-callback 'python-django-qmgmt-kill-and-msg-callback) ;; South integration (defun python-django-qmgmt-open-migration-callback (args) "Callback for commands that create migrations. Argument ARGS is an alist with the arguments passed to the management command." (when python-django-qmgmt-process-status-ok (let ((app (cdr (assq 'app args)))) (python-django-qmgmt-kill-and-msg-callback 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") (app (python-django-minibuffer-read-app "Convert App: "))) (defalias 'python-django-qmgmt-convert_to_south-callback 'python-django-qmgmt-kill-and-msg-callback) (python-django-qmgmt-define datamigration "Create a new datamigration for the given app." (:submenu "South" :binding "sod") (app (python-django-minibuffer-read-app "Datamigration for App: ")) (name "Datamigration name: ")) (defalias 'python-django-qmgmt-datamigration-callback 'python-django-qmgmt-open-migration-callback) (python-django-qmgmt-define migrate-all "Run all migrations for all apps." (:submenu "South" :switches "--all" :binding "somp") (database (python-django-minibuffer-read-database "Database: " default) "default" "--database=")) (defalias 'python-django-qmgmt-migrate-all-callback 'python-django-qmgmt-kill-and-msg-callback) (python-django-qmgmt-define migrate-app "Run all migrations for given app." (:submenu "South" :binding "soma") (database (python-django-minibuffer-read-database "Database: " default) "default" "--database=") (app (python-django-minibuffer-read-app "Migrate App: "))) (defalias 'python-django-qmgmt-migrate-app-callback 'python-django-qmgmt-kill-and-msg-callback) (python-django-qmgmt-define migrate-list "Run all migrations for all apps." (:submenu "South" :switches "--list" :binding "soml") (database (python-django-minibuffer-read-database "Database: " default) "default" "--database=")) (defalias 'python-django-qmgmt-migrate-list-callback 'python-django-qmgmt-kill-and-msg-callback) (python-django-qmgmt-define migrate-app-to "Run migrations for given app [up|down]-to given number." (:submenu "South" :binding "somt") (database (python-django-minibuffer-read-database "Database: " default) "default" "--database=") (app (python-django-minibuffer-read-app "Migrate App: " nil t)) (migration (python-django-minibuffer-read-migration "To migration: " app))) (defalias 'python-django-qmgmt-migrate-app-to-callback 'python-django-qmgmt-kill-and-msg-callback) (python-django-qmgmt-define schemamigration-initial "Create the initial schemamigration for the given app." (:submenu "South" :switches "--initial" :binding "sosi") (app (python-django-minibuffer-read-app "Initial schemamigration for App: "))) (defalias 'python-django-qmgmt-schemamigration-initial-callback 'python-django-qmgmt-kill-and-msg-callback) (python-django-qmgmt-define schemamigration "Create new empty schemamigration for the given app." (:submenu "South" :switches "--empty" :binding "soss") (app (python-django-minibuffer-read-app "Initial schemamigration for App: ")) (name "Schemamigration name: ")) (defalias 'python-django-qmgmt-schemamigration-callback 'python-django-qmgmt-open-migration-callback) (python-django-qmgmt-define schemamigration-auto "Create an automatic schemamigration for the given app." (:submenu "South" :switches "--auto" :binding "sosa") (app (python-django-minibuffer-read-app "Auto schemamigration for App: "))) (defalias 'python-django-qmgmt-schemamigration-auto-callback 'python-django-qmgmt-open-migration-callback) ;;; 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: " nil t))) (let ((app (assq (intern app) (python-django-info-get-app-paths)))) (when app (goto-char (point-min)) (re-search-forward (format " %s" (car 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-setting "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* ((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