Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial import of chirpradio into Hg.

This code was previously in CHIRP's private svn repository.
  • Loading branch information...
commit 1d342b7a92ce8b74e0a594cf84d38522c7e07440 0 parents
trow authored
Showing with 31,249 additions and 0 deletions.
  1. +202 −0 COPYING
  2. +104 −0 README
  3. 0  __init__.py
  4. +16 −0 app.yaml
  5. +537 −0 appengine_django/__init__.py
  6. +25 −0 appengine_django/auth/__init__.py
  7. +31 −0 appengine_django/auth/decorators.py
  8. +36 −0 appengine_django/auth/middleware.py
  9. +172 −0 appengine_django/auth/models.py
  10. +62 −0 appengine_django/auth/templatetags.py
  11. +58 −0 appengine_django/auth/tests.py
  12. 0  appengine_django/conf/app_template/__init__.py
  13. +4 −0 appengine_django/conf/app_template/models.py
  14. +1 −0  appengine_django/conf/app_template/views.py
  15. +20 −0 appengine_django/db/__init__.py
  16. +150 −0 appengine_django/db/base.py
  17. +37 −0 appengine_django/db/creation.py
  18. +93 −0 appengine_django/mail.py
  19. 0  appengine_django/management/__init__.py
  20. 0  appengine_django/management/commands/__init__.py
  21. +49 −0 appengine_django/management/commands/console.py
  22. +36 −0 appengine_django/management/commands/flush.py
  23. +32 −0 appengine_django/management/commands/reset.py
  24. +52 −0 appengine_django/management/commands/rollback.py
  25. +77 −0 appengine_django/management/commands/runserver.py
  26. +43 −0 appengine_django/management/commands/startapp.py
  27. +71 −0 appengine_django/management/commands/testserver.py
  28. +51 −0 appengine_django/management/commands/update.py
  29. +52 −0 appengine_django/management/commands/vacuum_indexes.py
  30. +176 −0 appengine_django/models.py
  31. +26 −0 appengine_django/replacement_imp.py
  32. 0  appengine_django/serializer/__init__.py
  33. +130 −0 appengine_django/serializer/python.py
  34. +147 −0 appengine_django/serializer/xml.py
  35. 0  appengine_django/sessions/__init__.py
  36. 0  appengine_django/sessions/backends/__init__.py
  37. +82 −0 appengine_django/sessions/backends/db.py
  38. +22 −0 appengine_django/sessions/models.py
  39. +56 −0 appengine_django/tests/__init__.py
  40. +183 −0 appengine_django/tests/commands_test.py
  41. +37 −0 appengine_django/tests/core_test.py
  42. +62 −0 appengine_django/tests/db_test.py
  43. +43 −0 appengine_django/tests/memcache_test.py
  44. +110 −0 appengine_django/tests/model_test.py
  45. +313 −0 appengine_django/tests/serialization_test.py
  46. +200 −0 auth/__init__.py
  47. +65 −0 auth/decorators.py
  48. +151 −0 auth/forms.py
  49. +51 −0 auth/middleware.py
  50. +113 −0 auth/models.py
  51. +31 −0 auth/roles.py
  52. +335 −0 auth/tests.py
  53. +44 −0 auth/urls.py
  54. +284 −0 auth/views.py
  55. 0  common/__init__.py
  56. +12 −0 common/context_processors.py
  57. +25 −0 common/decorators.py
  58. +5 −0 django-extras/README
  59. 0  django-extras/__init__.py
  60. +40 −0 django-extras/_monkey_patch.py
  61. 0  django-extras/contrib/__init__.py
  62. +15 −0 django-extras/contrib/auth/__init__.py
  63. 0  django-extras/foo.py
  64. BIN  django.zip
  65. 0  landing_page/__init__.py
  66. +13 −0 landing_page/views.py
  67. +59 −0 main.py
  68. +38 −0 manage.py
  69. +3 −0  media/common/css/base.css
  70. +153 −0 media/ext_js/jquery-autocomplete/autocomplete_docs.txt
  71. +46 −0 media/ext_js/jquery-autocomplete/jquery.autocomplete.css
  72. +502 −0 media/ext_js/jquery-autocomplete/jquery.autocomplete.js
  73. +318 −0 media/ext_js/jquery-ui-themeroller/demo.html
  74. +4,241 −0 media/ext_js/jquery-ui-themeroller/jquery-1.3.1.js
  75. +8,970 −0 media/ext_js/jquery-ui-themeroller/jquery.ui.all.js
  76. BIN  media/ext_js/jquery-ui-themeroller/theme/images/ui-bg_flat_0_aaaaaa_40x100.png
  77. BIN  media/ext_js/jquery-ui-themeroller/theme/images/ui-bg_flat_75_ffffff_40x100.png
  78. BIN  media/ext_js/jquery-ui-themeroller/theme/images/ui-bg_glass_55_fbf9ee_1x400.png
  79. BIN  media/ext_js/jquery-ui-themeroller/theme/images/ui-bg_glass_65_ffffff_1x400.png
  80. BIN  media/ext_js/jquery-ui-themeroller/theme/images/ui-bg_glass_75_dadada_1x400.png
  81. BIN  media/ext_js/jquery-ui-themeroller/theme/images/ui-bg_glass_75_e6e6e6_1x400.png
  82. BIN  media/ext_js/jquery-ui-themeroller/theme/images/ui-bg_glass_95_fef1ec_1x400.png
  83. BIN  media/ext_js/jquery-ui-themeroller/theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png
  84. BIN  media/ext_js/jquery-ui-themeroller/theme/images/ui-icons_222222_256x240.png
  85. BIN  media/ext_js/jquery-ui-themeroller/theme/images/ui-icons_2e83ff_256x240.png
  86. BIN  media/ext_js/jquery-ui-themeroller/theme/images/ui-icons_454545_256x240.png
  87. BIN  media/ext_js/jquery-ui-themeroller/theme/images/ui-icons_888888_256x240.png
  88. BIN  media/ext_js/jquery-ui-themeroller/theme/images/ui-icons_cd0a0a_256x240.png
  89. +9 −0 media/ext_js/jquery-ui-themeroller/theme/ui.accordion.css
  90. +2 −0  media/ext_js/jquery-ui-themeroller/theme/ui.all.css
  91. +9 −0 media/ext_js/jquery-ui-themeroller/theme/ui.base.css
  92. +37 −0 media/ext_js/jquery-ui-themeroller/theme/ui.core.css
  93. +62 −0 media/ext_js/jquery-ui-themeroller/theme/ui.datepicker.css
  94. +13 −0 media/ext_js/jquery-ui-themeroller/theme/ui.dialog.css
  95. +4 −0 media/ext_js/jquery-ui-themeroller/theme/ui.progressbar.css
  96. +13 −0 media/ext_js/jquery-ui-themeroller/theme/ui.resizable.css
  97. +17 −0 media/ext_js/jquery-ui-themeroller/theme/ui.slider.css
  98. +9 −0 media/ext_js/jquery-ui-themeroller/theme/ui.tabs.css
  99. +243 −0 media/ext_js/jquery-ui-themeroller/theme/ui.theme.css
  100. +42 −0 media/ext_js/jquery-ui-themeroller/ui_demos.js
  101. +3,549 −0 media/ext_js/jquery/jquery-1.2.6.js
  102. +4,241 −0 media/ext_js/jquery/jquery-1.3.1.js
  103. +152 −0 media/ext_js/jquery/plugins/jquery.simulate.js
  104. +780 −0 media/ext_js/jquery/qunit/testrunner.js
  105. +120 −0 media/ext_js/jquery/qunit/testsuite.css
  106. +18 −0 media/volunteers/css/claim_tasks.css
  107. +53 −0 media/volunteers/js/chirp.js
  108. +30 −0 media/volunteers/js/claim_tasks.js
  109. +23 −0 media/volunteers/js/claim_tasks_stub_urls.js
  110. +148 −0 media/volunteers/js/meetings.js
  111. +70 −0 media/volunteers/js/meetings_stub_urls.js
  112. +90 −0 media/volunteers/js/meetings_test.js
  113. +110 −0 settings.py
  114. +16 −0 templates/auth/change_password.html
  115. +19 −0 templates/auth/forgot_password.html
  116. +8 −0 templates/auth/forgot_password_email.txt
  117. +29 −0 templates/auth/hello.html
  118. +25 −0 templates/auth/list_users.html
  119. +32 −0 templates/auth/main_page.html
  120. +16 −0 templates/auth/reset_password.html
  121. +20 −0 templates/auth/user_form.html
  122. +40 −0 templates/common/base.html
  123. +11 −0 templates/landing_page/landing_page.html
  124. +42 −0 urls.py
  125. +2 −0  volunteers/__init__.py
  126. 0  volunteers/_ui_test_/__init__.py
  127. +97 −0 volunteers/_ui_test_/claim_tasks.py
  128. +36 −0 volunteers/_ui_test_/meetings.py
  129. +133 −0 volunteers/admin.py
  130. +103 −0 volunteers/fixtures/initial_data.json
  131. +81 −0 volunteers/forms.py
  132. +231 −0 volunteers/models.py
  133. 0  volunteers/templatetags/__init__.py
  134. +124 −0 volunteers/templatetags/volunteers_extras.py
  135. +5 −0 volunteers/tests/__init__.py
  136. +34 −0 volunteers/tests/base.py
  137. +422 −0 volunteers/tests/fixtures/event_tasks.json
  138. +9 −0 volunteers/tests/fixtures/meetings.json
  139. +23 −0 volunteers/tests/fixtures/tasks.json
  140. +92 −0 volunteers/tests/fixtures/users.json
  141. +28 −0 volunteers/tests/fixtures/volunteers.json
  142. +110 −0 volunteers/tests/test_coord_admin.py
  143. +82 −0 volunteers/tests/test_meeting_attendance.py
  144. +177 −0 volunteers/tests/test_volunteer_actions.py
  145. +50 −0 volunteers/tests/test_volunteer_extras.py
  146. +33 −0 volunteers/urls.py
  147. +163 −0 volunteers/views.py
202 COPYING
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
104 README
@@ -0,0 +1,104 @@
+
+This is the code for CHIRP's internal web applications. The apps are
+hosted on Google's App Engine, under the 'chirpradio' project.
+
+For the chirpradio developer dashboard, go to:
+http://appengine.google.com/dashboard?&app_id=chirpradio
+
+For the end-user landing page, go to:
+http://chirpradio.appspot.com/
+
+Helpful documentation:
+
+* App Engine Python API
+ http://code.google.com/appengine/docs/python/
+
+* Django 1.0:
+ http://www.djangobook.com/en/2.0/
+ Be sure you are looking at the right version of the book! We are using
+ version 1.0.
+
+
+CODING CONVENTIONS
+==================
+
+Please follow the conventions outlined in PEP 8.
+
+
+OVERVIEW OF THE TREE
+====================
+
+There are part of the common infrastructure.
+ djzango.zip
+ Django 1.0.2-final, zipped up. We never want to change this.
+ appengine_django/
+ From google-app-engine-django, AppEngine helper & glue code.
+ common/
+ Code & data shared by all apps.
+ django-extras/
+ A tree that is merged into the django namespace. We put our own
+ glue code here. This should be kept small and simple.
+ __init__.py
+ main.py
+ manage.py
+ Launchers for Django.
+ settings.py
+ Global configuration for Django.
+ urls.py
+ Main URL file.
+ auth/
+ Our own custom authentication & account management system.
+ media/ext_js/[package name]
+ External third-party Javascript packages (like JQuery) live under this
+ directory.
+
+These are places where all applications store data.
+ media/[application name]/{js, css, img}/
+ templates/[application name]/
+
+These are applications that are running in production.
+ (None so far)
+
+These are applications that are under development.
+ landing_page/
+ Where you end up when you go to "/". Currently a test page.
+ volunteers/
+ Volunteer tracking.
+
+
+ADDING A NEW APPLICATION
+========================
+
+Every application has a name that looks like this: "landing_page".
+Your code lives in a directory with the same name.
+Your templates go under the directory templates/[application name].
+Your media files go under the directory media/[application name].
+
+All of your URLs are automatically mapped to be under
+http://host/volunteers/my/url
+
+To make your URLs visible, you need to:
+(1) Update the top-level urls.py to include your urls.
+(2) Add your application to INSTALLED_APPS in settings.py.
+
+
+THIRD-PARTY CODE
+================
+
+Some of the files in this directory and all of files under the
+appengine_django/ subdirectory are based on rev 81 of the
+google-app-engine-django Subversion repository.
+
+All files in django.zip are taken from Django 1.0.2-final. It was
+constructed by running the following commands:
+
+zip -r django.zip django/__init__.py django/bin django/core \
+ django/db django/dispatch django/forms \
+ django/http django/middleware django/shortcuts \
+ django/template django/templatetags \
+ django/test django/utils django/views
+
+zip -r django.zip django/conf -x 'django/conf/locale/*'
+
+These commands were taken from
+http://code.google.com/appengine/articles/django10_zipimport.html
0  __init__.py
No changes.
16 app.yaml
@@ -0,0 +1,16 @@
+application: chirpradio
+version: 1
+runtime: python
+api_version: 1
+
+handlers:
+- url: /static
+ static_dir: static
+
+# Always use HTTPS for pages related to authentication & user management
+- url: /auth/.*
+ secure: always
+ script: main.py
+
+- url: /.*
+ script: main.py
537 appengine_django/__init__.py
@@ -0,0 +1,537 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Support for integrating a Django project with the appengine infrastructure.
+
+This requires Django 1.0beta1 or greater.
+
+This module enables you to use the Django manage.py utility and *some* of it's
+subcommands. View the help of manage.py for exact details.
+
+Additionally this module takes care of initialising the datastore (and a test
+datastore) so that the Django test infrastructure can be used for your
+appengine project.
+
+To use this module add the following two lines to your main.py and manage.py
+scripts at the end of your imports:
+ from appengine_django import InstallAppengineHelperForDjango
+ InstallAppengineHelperForDjango()
+
+If you would like to use a version of Django other than that provided by the
+system all you need to do is include it in a directory just above this helper,
+eg:
+ appengine_django/__init__.py - This file
+ django/... - your private copy of Django.
+"""
+
+import logging
+import os
+import re
+import sys
+import unittest
+import zipfile
+
+
+DIR_PATH = os.path.abspath(os.path.dirname(__file__))
+PARENT_DIR = os.path.dirname(DIR_PATH)
+if PARENT_DIR.endswith(".zip"):
+ # Check for appengine_django itself being in a zipfile.
+ PARENT_DIR = os.path.dirname(PARENT_DIR)
+
+# Add this project to the start of sys path to enable direct imports.
+sys.path = [PARENT_DIR,] + sys.path
+
+# Try to import the appengine code from the system path.
+try:
+ from google.appengine.api import apiproxy_stub_map
+except ImportError, e:
+ # Not on the system path. Build a list of alternative paths where it may be.
+ # First look within the project for a local copy, then look for where the Mac
+ # OS SDK installs it.
+ paths = [os.path.join(PARENT_DIR, '.google_appengine'),
+ os.path.join(PARENT_DIR, 'google_appengine'),
+ '/usr/local/google_appengine']
+ # Then if on windows, look for where the Windows SDK installed it.
+ for path in os.environ.get('PATH', '').split(';'):
+ path = path.rstrip('\\')
+ if path.endswith('google_appengine'):
+ paths.append(path)
+ try:
+ from win32com.shell import shell
+ from win32com.shell import shellcon
+ id_list = shell.SHGetSpecialFolderLocation(
+ 0, shellcon.CSIDL_PROGRAM_FILES)
+ program_files = shell.SHGetPathFromIDList(id_list)
+ paths.append(os.path.join(program_files, 'Google',
+ 'google_appengine'))
+ except ImportError, e:
+ # Not windows.
+ pass
+ # Loop through all possible paths and look for the SDK dir.
+ SDK_PATH = None
+ for sdk_path in paths:
+ if os.path.exists(sdk_path):
+ SDK_PATH = os.path.realpath(sdk_path)
+ break
+ if SDK_PATH is None:
+ # The SDK could not be found in any known location.
+ sys.stderr.write("The Google App Engine SDK could not be found!\n")
+ sys.stderr.write("See README for installation instructions.\n")
+ sys.exit(1)
+ if SDK_PATH == os.path.join(PARENT_DIR, 'google_appengine'):
+ logging.warn('Loading the SDK from the \'google_appengine\' subdirectory '
+ 'is now deprecated!')
+ logging.warn('Please move the SDK to a subdirectory named '
+ '\'.google_appengine\' instead.')
+ logging.warn('See README for further details.')
+ # Add the SDK and the libraries within it to the system path.
+ EXTRA_PATHS = [
+ SDK_PATH,
+ os.path.join(SDK_PATH, 'lib', 'antlr3'),
+ os.path.join(SDK_PATH, 'lib', 'django'),
+ os.path.join(SDK_PATH, 'lib', 'webob'),
+ os.path.join(SDK_PATH, 'lib', 'yaml', 'lib'),
+ ]
+ # Add SDK paths at the start of sys.path, but after the local directory which
+ # was added to the start of sys.path on line 50 above. The local directory
+ # must come first to allow the local imports to override the SDK and
+ # site-packages directories.
+ sys.path = sys.path[0:1] + EXTRA_PATHS + sys.path[1:]
+ from google.appengine.api import apiproxy_stub_map
+
+# Look for a zipped copy of Django.
+have_django_zip = False
+django_zip_path = os.path.join(PARENT_DIR, 'django.zip')
+if os.path.exists(django_zip_path):
+ have_django_zip = True
+ sys.path.insert(1, django_zip_path)
+
+# Remove the standard version of Django if a local copy has been provided.
+if have_django_zip or os.path.exists(os.path.join(PARENT_DIR, 'django')):
+ for k in [k for k in sys.modules if k.startswith('django')]:
+ del sys.modules[k]
+
+# Must set this env var *before* importing any more of Django.
+os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
+
+from django import VERSION
+from django.conf import settings
+
+from google.appengine.api import yaml_errors
+
+# Flags made available this module
+appid = None
+have_appserver = False
+
+# Hide everything other than the flags above and the install function.
+__all__ = ("appid", "have_appserver", "have_django_zip",
+ "django_zip_path", "InstallAppengineHelperForDjango")
+
+
+INCOMPATIBLE_COMMANDS = ["adminindex", "createcachetable", "dbshell",
+ "inspectdb", "runfcgi", "syncdb", "validate"]
+
+
+def LoadAppengineEnvironment():
+ """Loads the appengine environment.
+
+ Returns:
+ This function has no return value, but it sets the following parameters on
+ this package:
+ - appid: The name of the application.
+ - have_appserver: Boolean parameter which is True if the code is being run
+ from within the appserver environment.
+ """
+ global appid, have_appserver
+
+ # Detect if we are running under an appserver.
+ have_appserver = False
+ stub = apiproxy_stub_map.apiproxy.GetStub("datastore_v3")
+ if stub:
+ have_appserver = True
+
+ # Load the application identifier.
+ if have_appserver:
+ appid = os.environ.get("APPLICATION_ID", "unknown")
+ else:
+ # Running as manage.py script, read from config file.
+ try:
+ from google.appengine.tools import dev_appserver
+ appconfig, unused_matcher = dev_appserver.LoadAppConfig(PARENT_DIR, {})
+ appid = appconfig.application
+ except (ImportError, yaml_errors.EventListenerYAMLError), e:
+ logging.warn("Could not read the Application ID from app.yaml. "
+ "This may break things in unusual ways!")
+ # Something went wrong.
+ appid = "unknown"
+
+ logging.debug("Loading application '%s' %s an appserver" %
+ (appid, have_appserver and "with" or "without"))
+
+
+def InstallAppengineDatabaseBackend():
+ """Installs the appengine database backend into Django.
+
+ The appengine database lives in the db/ subdirectory of this package, but is
+ known as "appengine" to Django. This function installs the module where
+ Django expects to find its database backends.
+ """
+ from appengine_django import db
+ sys.modules['django.db.backends.appengine'] = db
+ logging.debug("Installed appengine database backend")
+
+
+def InstallGoogleMemcache():
+ """Installs the Google memcache into Django.
+
+ By default django tries to import standard memcache module.
+ Because appengine memcache is API compatible with Python memcache module,
+ we can trick Django to think it is installed and to use it.
+
+ Now you can use CACHE_BACKEND = 'memcached://' in settings.py. IP address
+ and port number are not required.
+ """
+ from google.appengine.api import memcache
+ sys.modules['memcache'] = memcache
+ logging.debug("Installed App Engine memcache backend")
+
+
+def InstallDjangoModuleReplacements():
+ """Replaces internal Django modules with App Engine compatible versions."""
+
+ # Replace the session module with a partial replacement overlay using
+ # __path__ so that portions not replaced will fall through to the original
+ # implementation.
+ try:
+ from django.contrib import sessions
+ orig_path = sessions.__path__[0]
+ sessions.__path__.insert(0, os.path.join(DIR_PATH, 'sessions'))
+ from django.contrib.sessions import backends
+ backends.__path__.append(os.path.join(orig_path, 'backends'))
+ except ImportError:
+ logging.debug("No Django session support available")
+
+ # Replace incompatible dispatchers.
+ import django.core.signals
+ import django.db
+ import django.dispatch.dispatcher
+
+ # Rollback occurs automatically on Google App Engine. Disable the Django
+ # rollback handler.
+ try:
+ # pre 1.0
+ from django.dispatch import errors
+ CheckedException = errors.DispatcherKeyError
+ def _disconnectSignal():
+ django.dispatch.dispatcher.disconnect(
+ django.db._rollback_on_exception,
+ django.core.signals.got_request_exception)
+ except ImportError:
+ CheckedException = KeyError
+ def _disconnectSignal():
+ django.core.signals.got_request_exception.disconnect(
+ django.db._rollback_on_exception)
+
+ try:
+ _disconnectSignal()
+ except CheckedException, e:
+ logging.debug("Django rollback handler appears to be already disabled.")
+
+def PatchDjangoSerializationModules():
+ """Monkey patches the Django serialization modules.
+
+ The standard Django serialization modules to not correctly handle the
+ datastore models provided by this package. This method installs replacements
+ for selected modules and methods to give Django the capability to correctly
+ serialize and deserialize datastore models.
+ """
+ # These can't be imported until InstallAppengineDatabaseBackend has run.
+ from django.core.serializers import python
+ from appengine_django.serializer.python import Deserializer
+ if not hasattr(settings, "SERIALIZATION_MODULES"):
+ settings.SERIALIZATION_MODULES = {}
+ base_module = "appengine_django"
+ settings.SERIALIZATION_MODULES["xml"] = "%s.serializer.xml" % base_module
+ python.Deserializer = Deserializer
+ PatchDeserializedObjectClass()
+ DisableModelValidation()
+ logging.debug("Installed appengine json and python serialization modules")
+
+
+def PatchDeserializedObjectClass():
+ """Patches the DeserializedObject class.
+
+ The default implementation calls save directly on the django Model base
+ class to avoid pre-save handlers. The model class provided by this package
+ is not derived from the Django Model class and therefore must be called
+ directly.
+
+ Additionally we need to clear the internal _parent attribute as it may
+ contain a FakeParent class that is used to deserialize instances without
+ needing to load the parent instance itself. See the PythonDeserializer for
+ more details.
+ """
+ # This can't be imported until InstallAppengineDatabaseBackend has run.
+ from django.core.serializers import base
+ class NewDeserializedObject(base.DeserializedObject):
+ def save(self, save_m2m=True):
+ self.object.save()
+ self.object._parent = None
+ base.DeserializedObject = NewDeserializedObject
+ logging.debug("Replacement DeserializedObject class installed")
+
+def DisableModelValidation():
+ """Disables Django's model validation routines.
+
+ The model validation is primarily concerned with validating foreign key
+ references. There is no equivalent checking code for datastore References at
+ this time.
+
+ Validation needs to be disabled or serialization/deserialization will fail.
+ """
+ from django.core.management import validation
+ validation.get_validation_errors = lambda x, y=0: 0
+ logging.debug("Django SQL model validation disabled")
+
+def CleanupDjangoSettings():
+ """Removes incompatible entries from the django settings module."""
+
+ # Ensure this module is installed as an application.
+ apps = getattr(settings, "INSTALLED_APPS", ())
+ found = False
+ for app in apps:
+ if app.endswith("appengine_django"):
+ found = True
+ break
+ if not found:
+ logging.warn("appengine_django module is not listed as an application!")
+ apps += ("appengine_django",)
+ setattr(settings, "INSTALLED_APPS", apps)
+ logging.info("Added 'appengine_django' as an application")
+
+ # Ensure the database backend is appropriately configured.
+ dbe = getattr(settings, "DATABASE_ENGINE", "")
+ if dbe != "appengine":
+ settings.DATABASE_ENGINE = "appengine"
+ logging.warn("DATABASE_ENGINE is not configured as 'appengine'. "
+ "Value overriden!")
+ for var in ["NAME", "USER", "PASSWORD", "HOST", "PORT"]:
+ val = getattr(settings, "DATABASE_%s" % var, "")
+ if val:
+ setattr(settings, "DATABASE_%s" % var, "")
+ logging.warn("DATABASE_%s should be blank. Value overriden!")
+
+ # Remove incompatible middleware modules.
+ mw_mods = list(getattr(settings, "MIDDLEWARE_CLASSES", ()))
+ disallowed_middleware_mods = (
+ 'django.middleware.doc.XViewMiddleware',)
+ for modname in mw_mods[:]:
+ if modname in disallowed_middleware_mods:
+ # Currently only the CommonMiddleware has been ported. As other base
+ # modules are converted, remove from the disallowed_middleware_mods
+ # tuple.
+ mw_mods.remove(modname)
+ logging.warn("Middleware module '%s' is not compatible. Removed!" %
+ modname)
+ setattr(settings, "MIDDLEWARE_CLASSES", tuple(mw_mods))
+
+ # Remove incompatible application modules
+ app_mods = list(getattr(settings, "INSTALLED_APPS", ()))
+ disallowed_apps = (
+ 'django.contrib.contenttypes',
+ 'django.contrib.sites',)
+ for app in app_mods[:]:
+ if app in disallowed_apps:
+ app_mods.remove(app)
+ logging.warn("Application module '%s' is not compatible. Removed!" % app)
+ setattr(settings, "INSTALLED_APPS", tuple(app_mods))
+
+ # Remove incompatible session backends.
+ session_backend = getattr(settings, "SESSION_ENGINE", "")
+ if session_backend.endswith("file"):
+ logging.warn("File session backend is not compatible. Overriden "
+ "to use db backend!")
+ setattr(settings, "SESSION_ENGINE", "django.contrib.sessions.backends.db")
+
+
+def ModifyAvailableCommands():
+ """Removes incompatible commands and installs replacements where possible."""
+ if have_appserver:
+ # Commands are not used when running from an appserver.
+ return
+ from django.core import management
+ project_directory = os.path.join(__path__[0], "../")
+ if have_django_zip:
+ FindCommandsInZipfile.orig = management.find_commands
+ management.find_commands = FindCommandsInZipfile
+ management.get_commands()
+ # Replace startapp command which is set by previous call to get_commands().
+ from appengine_django.management.commands.startapp import ProjectCommand
+ management._commands['startapp'] = ProjectCommand(project_directory)
+ RemoveCommands(management._commands)
+ logging.debug("Removed incompatible Django manage.py commands")
+
+
+def FindCommandsInZipfile(management_dir):
+ """
+ Given a path to a management directory, returns a list of all the command
+ names that are available.
+
+ This implementation also works when Django is loaded from a zip.
+
+ Returns an empty list if no commands are defined.
+ """
+ zip_marker = ".zip%s" % os.sep
+ if zip_marker not in management_dir:
+ return FindCommandsInZipfile.orig(management_dir)
+
+ # Django is sourced from a zipfile, ask zip module for a list of files.
+ filename, path = management_dir.split(zip_marker)
+ zipinfo = zipfile.ZipFile("%s.zip" % filename)
+
+ # Add commands directory to management path.
+ path = os.path.join(path, "commands")
+
+ # The zipfile module returns paths in the format of the operating system
+ # that created the zipfile! This may not match the path to the zipfile
+ # itself. Convert operating system specific characters to a standard
+ # character (#) to compare paths to work around this.
+ path_normalise = re.compile(r"[/\\]")
+ path = path_normalise.sub("#", path)
+ def _IsCmd(t):
+ """Returns true if t matches the criteria for a command module."""
+ filename = os.path.basename(t)
+ t = path_normalise.sub("#", t)
+ if not t.startswith(path):
+ return False
+ if filename.startswith("_") or not t.endswith(".py"):
+ return False
+ return True
+
+ return [os.path.basename(f)[:-3] for f in zipinfo.namelist() if _IsCmd(f)]
+
+
+def RemoveCommands(command_dict):
+ """Removes incompatible commands from the specified command dictionary."""
+ for cmd in command_dict.keys():
+ if cmd.startswith("sql"):
+ del command_dict[cmd]
+ elif cmd in INCOMPATIBLE_COMMANDS:
+ del command_dict[cmd]
+
+
+def InstallReplacementImpModule():
+ """Install a replacement for the imp module removed by the appserver.
+
+ This is only to find mangement modules provided by applications.
+ """
+ if not have_appserver:
+ return
+ modname = 'appengine_django.replacement_imp'
+ imp_mod = __import__(modname, {}, [], [''])
+ sys.modules['imp'] = imp_mod
+ logging.debug("Installed replacement imp module")
+
+
+def InstallAppengineHelperForDjango():
+ """Installs and Patches all of the classes/methods required for integration.
+
+ If the variable DEBUG_APPENGINE_DJANGO is set in the environment verbose
+ logging of the actions taken will be enabled.
+ """
+ if VERSION < (1, 0, None):
+ logging.error("Django 1.0 or greater is required!")
+ sys.exit(1)
+
+ if os.getenv("DEBUG_APPENGINE_DJANGO"):
+ logging.getLogger().setLevel(logging.DEBUG)
+ else:
+ logging.getLogger().setLevel(logging.INFO)
+ logging.debug("Loading the Google App Engine Helper for Django...")
+
+ # Force Django to reload its settings.
+ settings._target = None
+
+ LoadAppengineEnvironment()
+ InstallReplacementImpModule()
+ InstallAppengineDatabaseBackend()
+ InstallModelForm()
+ InstallGoogleMemcache()
+ InstallDjangoModuleReplacements()
+ PatchDjangoSerializationModules()
+ CleanupDjangoSettings()
+ ModifyAvailableCommands()
+ InstallGoogleSMTPConnection()
+ InstallAuthentication()
+
+ logging.debug("Successfully loaded the Google App Engine Helper for Django.")
+
+
+def InstallGoogleSMTPConnection():
+ from appengine_django import mail as gmail
+ from django.core import mail
+ logging.debug("Installing Google Email Adapter for Django")
+ mail.SMTPConnection = gmail.GoogleSMTPConnection
+ mail.mail_admins = gmail.mail_admins
+ mail.mail_managers = gmail.mail_managers
+
+
+def InstallAuthentication():
+ if "django.contrib.auth" not in settings.INSTALLED_APPS:
+ return
+ try:
+ from appengine_django.auth import models as helper_models
+ from django.contrib.auth import models
+ models.User = helper_models.User
+ models.Group = helper_models.Group
+ models.Permission = helper_models.Permission
+ models.Message = helper_models.Message
+ from django.contrib.auth import middleware as django_middleware
+ from appengine_django.auth.middleware import AuthenticationMiddleware
+ django_middleware.AuthenticationMiddleware = AuthenticationMiddleware
+ from django.contrib.auth import decorators as django_decorators
+ from appengine_django.auth.decorators import login_required
+ django_decorators.login_required = login_required
+ from django.contrib import auth as django_auth
+ from django.contrib.auth import tests as django_tests
+ django_auth.suite = unittest.TestSuite
+ django_tests.suite = unittest.TestSuite
+ logging.debug("Installing authentication framework")
+ except ImportError:
+ logging.debug("No Django authentication support available")
+
+
+def InstallModelForm():
+ """Replace Django ModelForm with the AppEngine ModelForm."""
+ # This MUST happen as early as possible, but after any auth model patching.
+ from google.appengine.ext.db import djangoforms as aeforms
+ try:
+ # pre 1.0
+ from django import newforms as forms
+ except ImportError:
+ from django import forms
+
+ forms.ModelForm = aeforms.ModelForm
+
+ # Extend ModelForm with support for EmailProperty
+ # TODO: This should be submitted to the main App Engine SDK.
+ from google.appengine.ext.db import EmailProperty
+ def get_form_field(self, **kwargs):
+ """Return a Django form field appropriate for an email property."""
+ defaults = {'form_class': forms.EmailField}
+ defaults.update(kwargs)
+ return super(EmailProperty, self).get_form_field(**defaults)
+ EmailProperty.get_form_field = get_form_field
25 appengine_django/auth/__init__.py
@@ -0,0 +1,25 @@
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Authentication module that mimics the behavior of Django's authentication
+implementation.
+
+Limitations:
+ - all user permissions methods are not available (requires contenttypes)
+"""
+
+from django.template import add_to_builtins
+
+add_to_builtins('appengine_django.auth.templatetags')
31 appengine_django/auth/decorators.py
@@ -0,0 +1,31 @@
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Decorators for the authentication framework."""
+
+from django.http import HttpResponseRedirect
+
+from google.appengine.api import users
+
+
+def login_required(function):
+ """Implementation of Django's login_required decorator.
+
+ The login redirect URL is always set to request.path
+ """
+ def login_required_wrapper(request, *args, **kw):
+ if request.user.is_authenticated():
+ return function(request, *args, **kw)
+ return HttpResponseRedirect(users.create_login_url(request.path))
+ return login_required_wrapper
36 appengine_django/auth/middleware.py
@@ -0,0 +1,36 @@
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from django.contrib.auth.models import AnonymousUser
+
+from google.appengine.api import users
+
+from appengine_django.auth.models import User
+
+
+class LazyUser(object):
+ def __get__(self, request, obj_type=None):
+ if not hasattr(request, '_cached_user'):
+ user = users.get_current_user()
+ if user:
+ request._cached_user = User.get_djangouser_for_user(user)
+ else:
+ request._cached_user = AnonymousUser()
+ return request._cached_user
+
+
+class AuthenticationMiddleware(object):
+ def process_request(self, request):
+ request.__class__.user = LazyUser()
+ return None
172 appengine_django/auth/models.py
@@ -0,0 +1,172 @@
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+App Engine compatible models for the Django authentication framework.
+"""
+
+from django.core import mail
+from django.core.exceptions import ImproperlyConfigured
+from django.db import models
+from django.utils.encoding import smart_str
+import urllib
+
+from django.db.models.manager import EmptyManager
+
+from google.appengine.api import users
+from google.appengine.ext import db
+
+from appengine_django.models import BaseModel
+
+
+class User(BaseModel):
+ """A model with the same attributes and methods as a Django user model.
+
+ The model has two additions. The first addition is a 'user' attribute which
+ references a App Engine user. The second is the 'get_djangouser_for_user'
+ classmethod that should be used to retrieve a DjangoUser instance from a App
+ Engine user object.
+ """
+ user = db.UserProperty(required=True)
+ username = db.StringProperty(required=True)
+ first_name = db.StringProperty()
+ last_name = db.StringProperty()
+ email = db.EmailProperty()
+ password = db.StringProperty()
+ is_staff = db.BooleanProperty(default=False, required=True)
+ is_active = db.BooleanProperty(default=True, required=True)
+ is_superuser = db.BooleanProperty(default=False, required=True)
+ last_login = db.DateTimeProperty(auto_now_add=True, required=True)
+ date_joined = db.DateTimeProperty(auto_now_add=True, required=True)
+ groups = EmptyManager()
+ user_permissions = EmptyManager()
+
+ def __unicode__(self):
+ return self.username
+
+ def __str__(self):
+ return unicode(self).encode('utf-8')
+
+ @classmethod
+ def get_djangouser_for_user(cls, user):
+ query = cls.all().filter("user =", user)
+ if query.count() == 0:
+ django_user = cls(user=user, email=user.email(), username=user.nickname())
+ django_user.save()
+ else:
+ django_user = query.get()
+ return django_user
+
+ def set_password(self, raw_password):
+ raise NotImplementedError
+
+ def check_password(self, raw_password):
+ raise NotImplementedError
+
+ def set_unusable_password(self):
+ raise NotImplementedError
+
+ def has_usable_password(self):
+ raise NotImplementedError
+
+ def get_group_permissions(self):
+ return self.user_permissions
+
+ def get_all_permissions(self):
+ return self.user_permissions
+
+ def has_perm(self, perm):
+ return False
+
+ def has_perms(self, perm_list):
+ return False
+
+ def has_module_perms(self, module):
+ return False
+
+ def get_and_delete_messages(self):
+ """Gets and deletes messages for this user"""
+ msgs = []
+ for msg in self.message_set:
+ msgs.append(msg)
+ msg.delete()
+ return msgs
+
+ def is_anonymous(self):
+ """Always return False"""
+ return False
+
+ def is_authenticated(self):
+ """Always return True"""
+ return True
+
+ def get_absolute_url(self):
+ return "/users/%s/" % urllib.quote(smart_str(self.username))
+
+ def get_full_name(self):
+ full_name = u'%s %s' % (self.first_name, self.last_name)
+ return full_name.strip()
+
+ def email_user(self, subject, message, from_email):
+ """Sends an email to this user.
+
+ According to the App Engine email API the from_email must be the
+ email address of a registered administrator for the application.
+ """
+ mail.send_mail(subject,
+ message,
+ from_email,
+ [self.email])
+
+ def get_profile(self):
+ """
+ Returns site-specific profile for this user. Raises
+ SiteProfileNotAvailable if this site does not allow profiles.
+
+ When using the App Engine authentication framework, users are created
+ automatically.
+ """
+ from django.contrib.auth.models import SiteProfileNotAvailable
+ if not hasattr(self, '_profile_cache'):
+ from django.conf import settings
+ if not hasattr(settings, "AUTH_PROFILE_MODULE"):
+ raise SiteProfileNotAvailable
+ try:
+ app_label, model_name = settings.AUTH_PROFILE_MODULE.split('.')
+ model = models.get_model(app_label, model_name)
+ self._profile_cache = model.all().filter("user =", self).get()
+ if not self._profile_cache:
+ raise model.DoesNotExist
+ except (ImportError, ImproperlyConfigured):
+ raise SiteProfileNotAvailable
+ return self._profile_cache
+
+
+class Group(BaseModel):
+ """Group model not fully implemented yet."""
+ # TODO: Implement this model, requires contenttypes
+ name = db.StringProperty()
+ permissions = EmptyManager()
+
+
+class Message(BaseModel):
+ """User message model"""
+ user = db.ReferenceProperty(User)
+ message = db.TextProperty()
+
+
+class Permission(BaseModel):
+ """Permission model not fully implemented yet."""
+ # TODO: Implement this model, requires contenttypes
+ name = db.StringProperty()
62 appengine_django/auth/templatetags.py
@@ -0,0 +1,62 @@
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Template tags for the auth module. These are inserted into Django as "built-in"
+tags so you do not need to use the load statement in your template to get
+access to them.
+"""
+
+from django.template import Library
+from django.template import Node
+
+from google.appengine.api import users
+
+
+class AuthLoginUrlsNode(Node):
+ """Template node that creates an App Engine login or logout URL.
+
+ If create_login_url is True the App Engine's login URL is rendered into
+ the template, otherwise the logout URL.
+ """
+ def __init__(self, create_login_url, redirect):
+ self.redirect = redirect
+ self.create_login_url = create_login_url
+
+ def render(self, context):
+ if self.create_login_url:
+ return users.create_login_url(self.redirect)
+ else:
+ return users.create_logout_url(self.redirect)
+
+
+def auth_login_urls(parser, token):
+ """Template tag registered as 'auth_login_url' and 'auth_logout_url'
+ when the module is imported.
+
+ Both tags take an optional argument that specifies the redirect URL and
+ defaults to '/'.
+ """
+ bits = list(token.split_contents())
+ if len(bits) == 2:
+ redirect = bits[1]
+ else:
+ redirect = "/"
+ login = bits[0] == "auth_login_url"
+ return AuthLoginUrlsNode(login, redirect)
+
+
+register = Library()
+register.tag("auth_login_url", auth_login_urls)
+register.tag("auth_logout_url", auth_login_urls)
58 appengine_django/auth/tests.py
@@ -0,0 +1,58 @@
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+BASIC_TESTS = """
+>>> from google.appengine.api import users
+>>> from models import User, AnonymousUser
+>>> appengine_user = users.User("test@example.com")
+>>> django_user = User.get_djangouser_for_user(appengine_user)
+>>> django_user.email == appengine_user.email()
+True
+>>> django_user.username == appengine_user.nickname()
+True
+>>> django_user.user == appengine_user
+True
+
+>>> django_user.username = 'test2'
+>>> key = django_user.save()
+>>> django_user.username == 'test2'
+True
+
+>>> django_user2 = User.get_djangouser_for_user(appengine_user)
+>>> django_user2 == django_user
+True
+
+>>> django_user.is_authenticated()
+True
+>>> django_user.is_staff
+False
+>>> django_user.is_active
+True
+
+>>> a = AnonymousUser()
+>>> a.is_authenticated()
+False
+>>> a.is_staff
+False
+>>> a.is_active
+False
+>>> a.groups.all()
+[]
+>>> a.user_permissions.all()
+[]
+
+
+"""
+
+__test__ = {'BASIC_TESTS': BASIC_TESTS}
0  appengine_django/conf/app_template/__init__.py
No changes.
4 appengine_django/conf/app_template/models.py
@@ -0,0 +1,4 @@
+from appengine_django.models import BaseModel
+from google.appengine.ext import db
+
+# Create your models here.
1  appengine_django/conf/app_template/views.py
@@ -0,0 +1 @@
+# Create your views here.
20 appengine_django/db/__init__.py
@@ -0,0 +1,20 @@
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Explicitly set the name of this package to "appengine".
+#
+# The rationale for this is so that Django can refer to the database as
+# "appengine" even though at a filesystem level it appears as the "db" package
+# within the appengine_django package.
+__name__ = "appengine"
150 appengine_django/db/base.py
@@ -0,0 +1,150 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This module looks after initialising the appengine api stubs."""
+
+import logging
+import os
+
+from appengine_django import appid
+from appengine_django import have_appserver
+from appengine_django.db.creation import DatabaseCreation
+
+
+from django.db.backends import BaseDatabaseWrapper
+from django.db.backends import BaseDatabaseFeatures
+from django.db.backends import BaseDatabaseOperations
+
+
+def get_datastore_paths():
+ """Returns a tuple with the path to the datastore and history file.
+
+ The datastore is stored in the same location as dev_appserver uses by
+ default, but the name is altered to be unique to this project so multiple
+ Django projects can be developed on the same machine in parallel.
+
+ Returns:
+ (datastore_path, history_path)
+ """
+ from google.appengine.tools import dev_appserver_main
+ datastore_path = dev_appserver_main.DEFAULT_ARGS['datastore_path']
+ history_path = dev_appserver_main.DEFAULT_ARGS['history_path']
+ datastore_path = datastore_path.replace("dev_appserver", "django_%s" % appid)
+ history_path = history_path.replace("dev_appserver", "django_%s" % appid)
+ return datastore_path, history_path
+
+
+def get_test_datastore_paths(inmemory=True):
+ """Returns a tuple with the path to the test datastore and history file.
+
+ If inmemory is true, (None, None) is returned to request an in-memory
+ datastore. If inmemory is false the path returned will be similar to the path
+ returned by get_datastore_paths but with a different name.
+
+ Returns:
+ (datastore_path, history_path)
+ """
+ if inmemory:
+ return None, None
+ datastore_path, history_path = get_datastore_paths()
+ datastore_path = datastore_path.replace("datastore", "testdatastore")
+ history_path = history_path.replace("datastore", "testdatastore")
+ return datastore_path, history_path
+
+
+def destroy_datastore(datastore_path, history_path):
+ """Destroys the appengine datastore at the specified paths."""
+ for path in [datastore_path, history_path]:
+ if not path: continue
+ try:
+ os.remove(path)
+ except OSError, e:
+ if e.errno != 2:
+ logging.error("Failed to clear datastore: %s" % e)
+
+
+class DatabaseError(Exception):
+ """Stub class for database errors. Required by Django"""
+ pass
+
+
+class IntegrityError(Exception):
+ """Stub class for database integrity errors. Required by Django"""
+ pass
+
+
+class DatabaseFeatures(BaseDatabaseFeatures):
+ """Stub class to provide the feaures member expected by Django"""
+ pass
+
+
+class DatabaseOperations(BaseDatabaseOperations):
+ """Stub class to provide the options member expected by Django"""
+ pass
+
+
+class DatabaseWrapper(BaseDatabaseWrapper):
+ """App Engine database definition for Django.
+
+ This "database" backend does not support any of the standard backend
+ operations. The only task that it performs is to setup the api stubs required
+ by the appengine libraries if they have not already been initialised by an
+ appserver.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(DatabaseWrapper, self).__init__(*args, **kwargs)
+ self.features = DatabaseFeatures()
+ self.ops = DatabaseOperations()
+ self.creation = DatabaseCreation(self)
+ self.use_test_datastore = kwargs.get("use_test_datastore", False)
+ self.test_datastore_inmemory = kwargs.get("test_datastore_inmemory", True)
+ if have_appserver:
+ return
+ self._setup_stubs()
+
+ def _get_paths(self):
+ if self.use_test_datastore:
+ return get_test_datastore_paths(self.test_datastore_inmemory)
+ else:
+ return get_datastore_paths()
+
+ def _setup_stubs(self):
+ # If this code is being run without an appserver (eg. via a django
+ # commandline flag) then setup a default stub environment.
+ from google.appengine.tools import dev_appserver_main
+ args = dev_appserver_main.DEFAULT_ARGS.copy()
+ args['datastore_path'], args['history_path'] = self._get_paths()
+ from google.appengine.tools import dev_appserver
+ dev_appserver.SetupStubs(appid, **args)
+ if self.use_test_datastore:
+ logging.debug("Configured API stubs for the test datastore")
+ else:
+ logging.debug("Configured API stubs for the development datastore")
+
+ def flush(self):
+ """Helper function to remove the current datastore and re-open the stubs"""
+ destroy_datastore(*self._get_paths())
+ self._setup_stubs()
+
+ def close(self):
+ pass
+
+ def _commit(self):
+ pass
+
+ def cursor(self, *args):
+ pass
37 appengine_django/db/creation.py
@@ -0,0 +1,37 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import logging
+
+from django.db.backends.creation import BaseDatabaseCreation
+
+
+class DatabaseCreation(BaseDatabaseCreation):
+
+ def create_test_db(self, *args, **kw):
+ """Destroys the test datastore. A new store will be recreated on demand"""
+ self.destroy_test_db()
+ self.connection.use_test_datastore = True
+ self.connection.flush()
+
+
+ def destroy_test_db(self, *args, **kw):
+ """Destroys the test datastore files."""
+ from appengine_django.db.base import destroy_datastore
+ from appengine_django.db.base import get_test_datastore_paths
+ destroy_datastore(*get_test_datastore_paths())
+ logging.debug("Destroyed test datastore")
93 appengine_django/mail.py
@@ -0,0 +1,93 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+This module replaces the Django mail implementation with a version that sends
+email via the mail API provided by Google App Engine.
+
+Multipart / HTML email is not yet supported.
+"""
+
+import logging
+
+from django.core import mail
+from django.core.mail import SMTPConnection
+from django.conf import settings
+
+from google.appengine.api import mail as gmail
+
+
+class GoogleSMTPConnection(SMTPConnection):
+ def __init__(self, host=None, port=None, username=None, password=None,
+ use_tls=None, fail_silently=False):
+ self.use_tls = (use_tls is not None) and use_tls or settings.EMAIL_USE_TLS
+ self.fail_silently = fail_silently
+ self.connection = None
+
+ def open(self):
+ self.connection = True
+
+ def close(self):
+ pass
+
+ def _send(self, email_message):
+ """A helper method that does the actual sending."""
+ if not email_message.to:
+ return False
+ try:
+ if (isinstance(email_message,gmail.EmailMessage)):
+ e = message
+ elif (isinstance(email_message,mail.EmailMessage)):
+ e = gmail.EmailMessage(sender=email_message.from_email,
+ to=email_message.to,
+ subject=email_message.subject,
+ body=email_message.body)
+ if email_message.bcc:
+ e.bcc = list(email_message.bcc)
+ #TODO - add support for html messages and attachments...
+ e.send()
+ except:
+ if not self.fail_silently:
+ raise
+ return False
+ return True
+
+
+def mail_admins(subject, message, fail_silently=False):
+ """Sends a message to the admins, as defined by the ADMINS setting."""
+ _mail_group(settings.ADMINS, subject, message, fail_silently)
+
+
+def mail_managers(subject, message, fail_silently=False):
+ """Sends a message to the managers, as defined by the MANAGERS setting."""
+ _mail_group(settings.MANAGERS, subject, message, fail_silently)
+
+
+def _mail_group(group, subject, message, fail_silently=False):
+ """Sends a message to an administrative group."""
+ if group:
+ mail.send_mail(settings.EMAIL_SUBJECT_PREFIX + subject, message,
+ settings.SERVER_EMAIL, [a[1] for a in group],
+ fail_silently)
+ return
+ # If the group had no recipients defined, default to the App Engine admins.
+ try:
+ gmail.send_mail_to_admins(settings.SERVER_EMAIL,
+ settings.EMAIL_SUBJECT_PREFIX + subject,
+ message)
+ except:
+ if not fail_silently:
+ raise
0  appengine_django/management/__init__.py
No changes.
0  appengine_django/management/commands/__init__.py
No changes.
49 appengine_django/management/commands/console.py
@@ -0,0 +1,49 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import code
+import getpass
+import os
+import sys
+
+from django.conf import settings
+from django.core.management.base import BaseCommand
+
+from google.appengine.ext.remote_api import remote_api_stub
+
+
+def auth_func():
+ return raw_input('Username:'), getpass.getpass('Password:')
+
+class Command(BaseCommand):
+ """ Start up an interactive console backed by your app using remote_api """
+
+ help = 'Start up an interactive console backed by your app using remote_api.'
+
+ def run_from_argv(self, argv):
+ app_id = argv[2]
+ if len(argv) > 3:
+ host = argv[3]
+ else:
+ host = '%s.appspot.com' % app_id
+
+ remote_api_stub.ConfigureRemoteDatastore(app_id,
+ '/remote_api',
+ auth_func,
+ host)
+
+ code.interact('App Engine interactive console for %s' % (app_id,),
+ None,
+ locals())
36 appengine_django/management/commands/flush.py
@@ -0,0 +1,36 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+import os
+import sys
+
+from django.core.management.base import BaseCommand
+
+
+class Command(BaseCommand):
+ """Overrides the default Django flush command.
+ """
+ help = 'Clears the current datastore and loads the initial fixture data.'
+
+ def run_from_argv(self, argv):
+ from django.db import connection
+ connection.flush()
+ from django.core.management import call_command
+ call_command('loaddata', 'initial_data')
+
+ def handle(self, *args, **kwargs):
+ self.run_from_argv(None)
32 appengine_django/management/commands/reset.py
@@ -0,0 +1,32 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import logging
+import os
+import sys
+
+from django.core.management.base import BaseCommand
+
+
+class Command(BaseCommand):
+ """Overrides the default Django reset command.
+ """
+ help = 'Clears the current datastore.'
+
+ def run_from_argv(self, argv):
+ from django.db import connection
+ connection.flush()
52 appengine_django/management/commands/rollback.py
@@ -0,0 +1,52 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import sys
+import logging
+
+from django.core.management.base import BaseCommand
+
+
+def run_appcfg():
+ # import this so that we run through the checks at the beginning
+ # and report the appropriate errors
+ import appcfg
+
+ # We don't really want to use that one though, it just executes this one
+ from google.appengine.tools import appcfg
+
+ # Reset the logging level to WARN as appcfg will spew tons of logs on INFO
+ logging.getLogger().setLevel(logging.WARN)
+
+ # Note: if we decide to change the name of this command to something other
+ # than 'rollback' we will have to munge the args to replace whatever
+ # we called it with 'rollback'
+ new_args = sys.argv[:]
+ new_args.append('.')
+ appcfg.main(new_args)
+
+
+class Command(BaseCommand):
+ """Calls the appcfg.py's rollback command for the current project.
+
+ Any additional arguments are passed directly to appcfg.py.
+ """
+ help = 'Calls appcfg.py rollback for the current project.'
+ args = '[any appcfg.py options]'
+
+ def run_from_argv(self, argv):
+ run_appcfg()
77 appengine_django/management/commands/runserver.py
@@ -0,0 +1,77 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import os
+import sys
+
+from appengine_django.db.base import get_datastore_paths
+
+from django.core.management.base import BaseCommand
+
+
+def start_dev_appserver():
+ """Starts the appengine dev_appserver program for the Django project.
+
+ The appserver is run with default parameters. If you need to pass any special
+ parameters to the dev_appserver you will have to invoke it manually.
+ """
+ from google.appengine.tools import dev_appserver_main
+ progname = sys.argv[0]
+ args = []
+ # hack __main__ so --help in dev_appserver_main works OK.
+ sys.modules['__main__'] = dev_appserver_main
+ # Set bind ip/port if specified.
+ if len(sys.argv) > 2:
+ addrport = sys.argv[2]
+ try:
+ addr, port = addrport.split(":")
+ except ValueError:
+ addr, port = None, addrport
+ if not port.isdigit():
+ print "Error: '%s' is not a valid port number." % port
+ sys.exit(1)
+ else:
+ addr, port = None, "8000"
+ if addr:
+ args.extend(["--address", addr])
+ if port:
+ args.extend(["--port", port])
+ # Add email settings
+ from django.conf import settings
+ args.extend(['--smtp_host', settings.EMAIL_HOST,
+ '--smtp_port', str(settings.EMAIL_PORT),
+ '--smtp_user', settings.EMAIL_HOST_USER,
+ '--smtp_password', settings.EMAIL_HOST_PASSWORD])
+ # Pass the application specific datastore location to the server.
+ p = get_datastore_paths()
+ args.extend(["--datastore_path", p[0], "--history_path", p[1]])
+ # Append the current working directory to the arguments.
+ dev_appserver_main.main([progname] + args + [os.getcwdu()])
+
+
+class Command(BaseCommand):
+ """Overrides the default Django runserver command.
+
+ Instead of starting the default Django development server this command
+ fires up a copy of the full fledged appengine dev_appserver that emulates
+ the live environment your application will be deployed to.
+ """
+ help = 'Runs a copy of the appengine development server.'
+ args = '[optional port number, or ipaddr:port]'
+
+ def run_from_argv(self, argv):
+ start_dev_appserver()
43 appengine_django/management/commands/startapp.py
@@ -0,0 +1,43 @@
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import os
+
+import django
+from django.core.management.commands import startapp
+
+import appengine_django
+
+
+class Command(startapp.Command):
+ def handle_label(self, *args, **kwds):
+ """Temporary adjust django.__path__ to load app templates from the
+ helpers directory.
+ """
+ old_path = django.__path__
+ django.__path__ = appengine_django.__path__
+ startapp.Command.handle_label(self, *args, **kwds)
+ django.__path__ = old_path
+
+
+class ProjectCommand(Command):
+ def __init__(self, project_directory):
+ super(ProjectCommand, self).__init__()
+ self.project_directory = project_directory
+
+ def handle_label(self, app_name, **options):
+ super(ProjectCommand, self).handle_label(app_name, self.project_directory,
+ **options)
+
71 appengine_django/management/commands/testserver.py
@@ -0,0 +1,71 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import os
+import sys
+
+from appengine_django.db.base import destroy_datastore
+from appengine_django.db.base import get_test_datastore_paths
+
+from django.core.management.base import BaseCommand
+
+
+class Command(BaseCommand):
+ """Overrides the default Django testserver command.
+
+ Instead of starting the default Django development server this command fires
+ up a copy of the full fledged appengine dev_appserver.
+
+ The appserver is always initialised with a blank datastore with the specified
+ fixtures loaded into it.
+ """
+ help = 'Runs the development server with data from the given fixtures.'
+
+ def run_from_argv(self, argv):
+ fixtures = argv[2:]
+
+ # Ensure an on-disk test datastore is used.
+ from django.db import connection
+ connection.use_test_datastore = True
+ connection.test_datastore_inmemory = False
+
+ # Flush any existing test datastore.
+ connection.flush()
+
+ # Load the fixtures.
+ from django.core.management import call_command
+ call_command('loaddata', 'initial_data')
+ if fixtures:
+ call_command('loaddata', *fixtures)
+
+ # Build new arguments for dev_appserver.
+ datastore_path, history_path = get_test_datastore_paths(False)
+ new_args = argv[0:1]
+ new_args.extend(['--datastore_path', datastore_path])
+ new_args.extend(['--history_path', history_path])
+ new_args.extend([os.getcwdu()])
+
+ # Add email settings
+ from django.conf import settings
+ new_args.extend(['--smtp_host', settings.EMAIL_HOST,
+ '--smtp_port', str(settings.EMAIL_PORT),
+ '--smtp_user', settings.EMAIL_HOST_USER,
+ '--smtp_password', settings.EMAIL_HOST_PASSWORD])
+
+ # Start the test dev_appserver.
+ from google.appengine.tools import dev_appserver_main
+ dev_appserver_main.main(new_args)
51 appengine_django/management/commands/update.py
@@ -0,0 +1,51 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import sys
+import logging
+
+from django.core.management.base import BaseCommand
+
+
+def run_appcfg():
+ # import this so that we run through the checks at the beginning
+ # and report the appropriate errors
+ import appcfg
+
+ # We don't really want to use that one though, it just executes this one
+ from google.appengine.tools import appcfg
+
+ # Reset the logging level to WARN as appcfg will spew tons of logs on INFO
+ logging.getLogger().setLevel(logging.WARN)
+
+ # Note: if we decide to change the name of this command to something other
+ # than 'update' we will have to munge the args to replace whatever
+ # we called it with 'update'
+ new_args = sys.argv[:]
+ new_args.append('.')
+ appcfg.main(new_args)
+
+class Command(BaseCommand):
+ """Calls the appcfg.py's update command for the current project.
+
+ Any additional arguments are passed directly to appcfg.py.
+ """
+ help = 'Calls appcfg.py update for the current project.'
+ args = '[any appcfg.py options]'
+
+ def run_from_argv(self, argv):
+ run_appcfg()
52 appengine_django/management/commands/vacuum_indexes.py
@@ -0,0 +1,52 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import sys
+import logging
+
+from django.core.management.base import BaseCommand
+
+
+def run_appcfg():
+ # import this so that we run through the checks at the beginning
+ # and report the appropriate errors
+ import appcfg
+
+ # We don't really want to use that one though, it just executes this one
+ from google.appengine.tools import appcfg
+
+ # Reset the logging level to WARN as appcfg will spew tons of logs on INFO
+ logging.getLogger().setLevel(logging.WARN)
+
+ # Note: if we decide to change the name of this command to something other
+ # than 'vacuum_indexes' we will have to munge the args to replace whatever
+ # we called it with 'vacuum_indexes'
+ new_args = sys.argv[:]
+ new_args.append('.')
+ appcfg.main(new_args)
+
+
+class Command(BaseCommand):
+ """Calls the appcfg.py's vacuum_indexes command for the current project.
+
+ Any additional arguments are passed directly to appcfg.py.
+ """
+ help = 'Calls appcfg.py vacuum_indexes for the current project.'
+ args = '[any appcfg.py options]'
+
+ def run_from_argv(self, argv):
+ run_appcfg()
176 appengine_django/models.py
@@ -0,0 +1,176 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+import types
+
+from google.appengine.ext import db
+
+from django import VERSION
+from django.core.exceptions import ObjectDoesNotExist
+from django.db.models.fields import Field
+from django.db.models.options import Options
+from django.db.models.loading import register_models, get_model
+
+
+class ModelManager(object):
+ """Replacement for the default Django model manager."""
+
+ def __init__(self, owner):
+ self.owner = owner
+
+ def __getattr__(self, name):
+ """Pass all attribute requests through to the real model"""
+ return getattr(self.owner, name)
+
+
+class ModelOptions(object):
+ """Replacement for the default Django options class.
+
+ This class sits at ._meta of each model. The primary information supplied by
+ this class that needs to be stubbed out is the list of fields on the model.
+ """
+
+ def __init__(self, cls):
+ self.object_name = cls.__name__
+ self.module_name = self.object_name.lower()
+ model_module = sys.modules[cls.__module__]
+ self.app_label = model_module.__name__.split('.')[-2]
+ self.abstract = False
+
+ class pk:
+ """Stub the primary key to always be 'key_name'"""
+ name = "key_name"
+
+ def __str__(self):
+ return "%s.%s" % (self.app_label, self.module_name)
+
+ @property
+ def many_to_many(self):
+ """The datastore does not support many to many relationships."""
+ return []
+
+
+class Relation(object):
+ def __init__(self, to):
+ self.field_name = "key_name"
+
+
+def PropertyWrapper(prop):
+ """Wrapper for db.Property to make it look like a Django model Property"""
+ if isinstance(prop, db.Reference):
+ prop.rel = Relation(prop.reference_class)
+ else:
+ prop.rel = None
+ prop.serialize = True
+ return prop
+
+
+class PropertiedClassWithDjango(db.PropertiedClass):
+ """Metaclass for the combined Django + App Engine model class.
+
+ This metaclass inherits from db.PropertiedClass in the appengine library.
+ This metaclass has two additional purposes:
+ 1) Register each model class created with Django (the parent class will take
+ care of registering it with the appengine libraries).
+ 2) Add the (minimum number) of attributes and methods to make Django believe
+ the class is a normal Django model.
+
+ The resulting classes are still not generally useful as Django classes and
+ are intended to be used by Django only in limited situations such as loading
+ and dumping fixtures.
+ """
+
+ def __new__(cls, name, bases, attrs):
+ """Creates a combined appengine and Django model.
+
+ The resulting model will be known to both the appengine libraries and
+ Django.
+ """
+ if name == 'BaseModel':
+ # This metaclass only acts on subclasses of BaseModel.
+ return super(PropertiedClassWithDjango, cls).__new__(cls, name,
+ bases, attrs)
+
+ new_class = super(PropertiedClassWithDjango, cls).__new__(cls, name,
+ bases, attrs)
+
+ new_class._meta = ModelOptions(new_class)
+ new_class.objects = ModelManager(new_class)
+ new_class._default_manager = new_class.objects
+ new_class.DoesNotExist = types.ClassType('DoesNotExist',
+ (ObjectDoesNotExist,), {})
+
+ m = get_model(new_class._meta.app_label, name, False)
+ if m:
+ return m
+
+ register_models(new_class._meta.app_label, new_class)
+ return get_model(new_class._meta.app_label, name, False)
+
+ def __init__(cls, name, bases, attrs):
+ """Initialises the list of Django properties.
+
+ This method takes care of wrapping the properties created by the superclass
+ so that they look like Django properties and installing them into the
+ ._meta object of the class so that Django can find them at the appropriate
+ time.
+ """
+ super(PropertiedClassWithDjango, cls).__init__(name, bases, attrs)
+ if name == 'BaseModel':
+ # This metaclass only acts on subclasses of BaseModel.
+ return
+
+ fields = [PropertyWrapper(p) for p in cls._properties.values()]
+ cls._meta.local_fields = fields
+
+
+class BaseModel(db.Model):
+ """Combined appengine and Django model.
+
+ All models used in the application should derive from this class.
+ """
+ __metaclass__ = PropertiedClassWithDjango
+
+ def __eq__(self, other):
+ if not isinstance(other, self.__class__):
+ return False
+ return self._get_pk_val() == other._get_pk_val()
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def _get_pk_val(self):
+ """Return the string representation of the model's key"""
+ return unicode(self.key())
+
+ def __repr__(self):
+ # Returns a string representation that can be used to construct an
+ # equivalent object. First, creates a dictionary of property names and
+ # values. Note that property values, not property objects, has to be passed
+ # in to constructor.
+ d = dict([(k, self.__getattribute__(k)) for k in self.properties()])
+ return "%s(**%s)" % (self.__class__.__name__, repr(d))
+
+
+class RegistrationTestModel(BaseModel):
+ """Used to check registration with Django is working correctly.
+
+ Django 0.96 only recognises models defined within an applications models
+ module when get_models() is called so this definition must be here rather
+ than within the associated test (tests/model_test.py).
+ """
+ pass
26 appengine_django/replacement_imp.py
@@ -0,0 +1,26 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This file acts as a very minimal replacement for the 'imp' module.
+
+It contains only what Django expects to use and does not actually implement the
+same functionality as the real 'imp' module.
+"""
+
+
+def find_module(name, path=None):
+ """Django needs imp.find_module, but it works fine if nothing is found."""
+ raise ImportError
0  appengine_django/serializer/__init__.py
No changes.
130 appengine_django/serializer/python.py
@@ -0,0 +1,130 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+A Python "serializer", based on the default Django python serializer.
+
+The only customisation is in the deserialization process which needs to take
+special care to resolve the name and parent attributes of the key for each
+entity and also recreate the keys for any references appropriately.
+"""
+
+
+from django.conf import settings
+from django.core.serializers import base
+from django.core.serializers import python
+from django.db import models
+
+from google.appengine.api import datastore_types
+from google.appengine.ext import db
+
+from django.utils.encoding import smart_unicode
+
+Serializer = python.Serializer
+
+
+class FakeParent(object):
+ """Fake parent 'model' like object.
+
+ This class exists to allow a parent object to be provided to a new model
+ without having to load the parent instance itself.
+ """
+
+ def __init__(self, parent_key):
+ self._entity = parent_key
+
+
+def Deserializer(object_list, **options):
+ """Deserialize simple Python objects back into Model instances.
+
+ It's expected that you pass the Python objects themselves (instead of a
+ stream or a string) to the constructor
+ """
+ models.get_apps()
+ for d in object_list:
+ # Look up the model and starting build a dict of data for it.
+ Model = python._get_model(d["model"])
+ data = {}
+ key = resolve_key(Model._meta.module_name, d["pk"])
+ if key.name():
+ data["key_name"] = key.name()
+ parent = None
+ if key.parent():
+ parent = FakeParent(key.parent())
+ m2m_data = {}
+
+ # Handle each field
+ for (field_name, field_value) in d["fields"].iteritems():
+ if isinstance(field_value, str):
+ field_value = smart_unicode(
+ field_value, options.get("encoding",
+ settings.DEFAULT_CHARSET),
+ strings_only=True)
+ field = Model.properties()[field_name]
+
+ if isinstance(field, db.Reference):
+ # Resolve foreign key references.
+ data[field.name] = resolve_key(Model._meta.module_name, field_value)
+ if not data[field.name].name():
+ raise base.DeserializationError(u"Cannot load Reference with "
+ "unnamed key: '%s'" % field_value)
+ else:
+ data[field.name] = field.validate(field_value)
+ # Create the new model instance with all it's data, but no parent.
+ object = Model(**data)
+ # Now add the parent into the hidden attribute, bypassing the type checks
+ # in the Model's __init__ routine.
+ object._parent = parent
+ # When the deserialized object is saved our replacement DeserializedObject
+ # class will set object._parent to force the real parent model to be loaded
+ # the first time it is referenced.
+ yield base.DeserializedObject(object, m2m_data)
+
+
+def resolve_key(model, key_data):
+ """Creates a Key instance from a some data.
+
+ Args:
+ model: The name of the model this key is being resolved for. Only