├── .gitignore ├── LICENSE ├── README.rst ├── docs ├── Makefile ├── TODO.rst ├── background.rst ├── conf.py ├── credits.rst ├── goals.rst ├── index.rst ├── make.bat ├── settings.rst ├── test_project.rst └── usage.rst ├── la_facebook ├── __init__.py ├── access.py ├── callbacks │ ├── __init__.py │ ├── base.py │ └── default.py ├── exceptions.py ├── la_fb_logging.py ├── models.py ├── templates │ └── la_facebook │ │ └── fb_error.html ├── templatetags │ ├── __init__.py │ └── la_facebook_tags.py ├── tests │ ├── __init__.py │ └── test_access_module.py ├── urls.py ├── utils │ ├── __init__.py │ ├── anyetree.py │ └── loader.py └── views.py ├── requirements.txt ├── setup.cfg ├── setup.py └── test_project ├── __init__.py ├── connect ├── __init__.py ├── fixtures │ └── initial_data.json ├── models.py ├── templates │ ├── after.html │ └── index.html ├── tests │ ├── __init__.py │ ├── test_connect.py │ └── utils.py ├── urls.py └── views.py ├── manage.py ├── settings.py └── urls.py /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | dist 3 | *pyc 4 | lafbenv/ 5 | test_project/dummy 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Daniel Greenfeld and contributors. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | README 3 | =========== 4 | 5 | Dedicated facebook authentication for Django that does it via the backend and not javascript. Has lots of tests and a trivial-to-setup test project with working code. 6 | 7 | The blog entry that started it all: 8 | 9 | http://pydanny.blogspot.com/2011/01/what-i-want-for-django-facebook-connect.html 10 | 11 | Full Documentation: 12 | 13 | http://django-la-facebook.readthedocs.org/en/latest/index.html 14 | 15 | Note: This is the old development fork. Work is now happening at: 16 | 17 | https://github.com/cartwheelweb/django-la-facebook -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-la-facebook.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-la-facebook.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-la-facebook" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-la-facebook" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/TODO.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | TODO 3 | ============ 4 | 5 | Code 6 | ----- 7 | 8 | #. User name determination rules 9 | #. more examples of callbacks 10 | 11 | Docs 12 | ---- 13 | 14 | #. Core readme 15 | #. docs/usage.txt (finish) 16 | #. access 17 | #. callbacks 18 | #. exceptions 19 | #. our settings 20 | #. views 21 | -------------------------------------------------------------------------------- /docs/background.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Background 3 | ========== 4 | 5 | This documents provides a quick overview of the events involved in 6 | authenticating a user against facebook. 7 | 8 | Facebook has had various authentication methods in the past (Facebook Connect), 9 | but has currently standardized on using `OAuth 2 Protocol 10 | `_. 11 | 12 | Facebook's `own documentation 13 | `_ does a reasonable job of 14 | explaining the process, but it is summarized here. Facebook offers two 15 | workflows for user authentication, client (javascript) based, and server side. 16 | This project aims to provide a Python based, server side option. 17 | 18 | There are three parts to Facebook's authentication: 19 | 20 | - user authentication (ensures that the user is who they say they are) 21 | - app authorization (ensures that the user knows exactly what data and capabilities they are providing to your site) 22 | - app authentication (ensures that the user is giving their information to your app and not someone else) 23 | 24 | Facebook will only authenticate a user in relation to a specific app, there is 25 | no "just authorize the user" option. In our case, the "app" that is 26 | authenticated is your entire Django project or site, not a specific Django app. 27 | For Facebook to associate your site with the authentication, you will need an 28 | app ID from `Facebook's Developer app `_, 29 | which is placed in your Django settings file. 30 | 31 | A user is authenticated to facebook, and your app in one step. The first time 32 | this happens, the user will be prompted to approve the level of access you are 33 | asking for. By default and at a minimum this includes the basic info that is 34 | publicly available about the user on Facebook. (for additional permissions, see 35 | the documentation for settings). The permissions approved will be global to all 36 | your Django apps in your Django project. 37 | 38 | Once your site is authorized by Facebook, an authorization token is stored in 39 | a model associated with a Django user (which by default is created if needed). 40 | That user that will then be the authenticated user for subsequent requests 41 | (request.user). 42 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-la-facebook documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Feb 5 13:26:37 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'django-la-facebook' 44 | copyright = u'2011, Daniel Greenfeld and contributors' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.1' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.1.alpha' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'django-la-facebookdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('index', 'django-la-facebook.tex', u'django-la-facebook Documentation', 182 | u'Daniel Greenfeld and contributors', 'manual'), 183 | ] 184 | 185 | # The name of an image file (relative to this directory) to place at the top of 186 | # the title page. 187 | #latex_logo = None 188 | 189 | # For "manual" documents, if this is true, then toplevel headings are parts, 190 | # not chapters. 191 | #latex_use_parts = False 192 | 193 | # If true, show page references after internal links. 194 | #latex_show_pagerefs = False 195 | 196 | # If true, show URL addresses after external links. 197 | #latex_show_urls = False 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | #latex_preamble = '' 201 | 202 | # Documents to append as an appendix to all manuals. 203 | #latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | #latex_domain_indices = True 207 | 208 | 209 | # -- Options for manual page output -------------------------------------------- 210 | 211 | # One entry per manual page. List of tuples 212 | # (source start file, name, description, authors, manual section). 213 | man_pages = [ 214 | ('index', 'django-la-facebook', u'django-la-facebook Documentation', 215 | [u'Daniel Greenfeld and contributors'], 1) 216 | ] 217 | -------------------------------------------------------------------------------- /docs/credits.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | django-la-facebook team 6 | ----------------------- 7 | 8 | * Alexandros Bantis 9 | * Daniel Greenfeld 10 | * David Peters 11 | * Jacob Burch 12 | * Preston Holmes 13 | 14 | django-oauth-access team 15 | ------------------------ 16 | 17 | *A lot of the core code of this project was liberally lifted from django-oauth-access*. We want to extend all gratitude and thanks to the guys who made that excellent project. 18 | 19 | * Brian Rosner 20 | * Patrick Altman 21 | -------------------------------------------------------------------------------- /docs/goals.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Goals 3 | ===== 4 | 5 | Security is HARD. 6 | Security is DANGEROUS. 7 | Doing authentication via a third-party JavaScript library is STUPID. 8 | 9 | Yet authentication via Facebook's JavaScript library is all over the place. 10 | 11 | The better way would be to do authentication via Facebook-flavored OAUTH on the backend. With a well documented, testable project complete with working code examples. The working code examples should be in a dirt simple test project. The test project allows a developer to quickly analyze why facebook auth is failing without the complications of working in their entire system stack. 12 | 13 | Our goals: 14 | 15 | #. Good documentation that will build on readthedocs.org. 16 | #. Proper logging for debug and intrusion analysis 17 | #. Working test projects. 18 | #. Working tests 19 | #. Formal releases on PyPI -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-la-facebook documentation master file, created by 2 | sphinx-quickstart on Sat Feb 5 13:26:37 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-la-facebook's documentation! 7 | ============================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | usage 15 | settings 16 | test_project 17 | background 18 | goals 19 | credits 20 | TODO 21 | 22 | External Links 23 | =============== 24 | 25 | Project homepage: 26 | 27 | https://github.com/pydanny/django-la-facebook 28 | 29 | PyPI: 30 | 31 | http://pypi.python.org/pypi/django-la-facebook 32 | 33 | Django Packages: 34 | 35 | http://djangopackages.com/packages/p/django-la-facebook/ 36 | 37 | Indices and tables 38 | ================== 39 | 40 | * :ref:`genindex` 41 | * :ref:`modindex` 42 | * :ref:`search` 43 | 44 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 47 | goto end 48 | ) 49 | 50 | if "%1" == "dirhtml" ( 51 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 52 | echo. 53 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 54 | goto end 55 | ) 56 | 57 | if "%1" == "singlehtml" ( 58 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 59 | echo. 60 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 61 | goto end 62 | ) 63 | 64 | if "%1" == "pickle" ( 65 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 66 | echo. 67 | echo.Build finished; now you can process the pickle files. 68 | goto end 69 | ) 70 | 71 | if "%1" == "json" ( 72 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 73 | echo. 74 | echo.Build finished; now you can process the JSON files. 75 | goto end 76 | ) 77 | 78 | if "%1" == "htmlhelp" ( 79 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 80 | echo. 81 | echo.Build finished; now you can run HTML Help Workshop with the ^ 82 | .hhp project file in %BUILDDIR%/htmlhelp. 83 | goto end 84 | ) 85 | 86 | if "%1" == "qthelp" ( 87 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 88 | echo. 89 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 90 | .qhcp project file in %BUILDDIR%/qthelp, like this: 91 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-la-facebook.qhcp 92 | echo.To view the help file: 93 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-la-facebook.ghc 94 | goto end 95 | ) 96 | 97 | if "%1" == "devhelp" ( 98 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 99 | echo. 100 | echo.Build finished. 101 | goto end 102 | ) 103 | 104 | if "%1" == "epub" ( 105 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 106 | echo. 107 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 108 | goto end 109 | ) 110 | 111 | if "%1" == "latex" ( 112 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 113 | echo. 114 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 115 | goto end 116 | ) 117 | 118 | if "%1" == "text" ( 119 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 120 | echo. 121 | echo.Build finished. The text files are in %BUILDDIR%/text. 122 | goto end 123 | ) 124 | 125 | if "%1" == "man" ( 126 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 127 | echo. 128 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 129 | goto end 130 | ) 131 | 132 | if "%1" == "changes" ( 133 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 134 | echo. 135 | echo.The overview file is in %BUILDDIR%/changes. 136 | goto end 137 | ) 138 | 139 | if "%1" == "linkcheck" ( 140 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 141 | echo. 142 | echo.Link check complete; look for any errors in the above output ^ 143 | or in %BUILDDIR%/linkcheck/output.txt. 144 | goto end 145 | ) 146 | 147 | if "%1" == "doctest" ( 148 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 149 | echo. 150 | echo.Testing of doctests in the sources finished, look at the ^ 151 | results in %BUILDDIR%/doctest/output.txt. 152 | goto end 153 | ) 154 | 155 | :end 156 | -------------------------------------------------------------------------------- /docs/settings.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Settings 3 | ======== 4 | 5 | This document describes the settings needed by la_facebook. 6 | 7 | FACEBOOK_ACCESS_SETTINGS this is the only setting required. It is a dictionary 8 | of app specific settings as follows: 9 | 10 | FACEBOOK_APP_ID - this is a key that uniquely identifies your Facebook app, in 11 | the common case, the Facebook app will be your project or site. 12 | 13 | FACEBOOK_APP_SECRET - This secret is used in generating the authenticated 14 | token. Both the key and the secret are available through the `Facebook's Developer app `_. 15 | 16 | CALLBACK (optional) - this is a dotted module path string (similar to using a string for 17 | a view) that points to a subclass of la_facebook.callbacks.default. The default 18 | value is "la_facebook.callbacks.default.default_facebook_callback" 19 | 20 | PROVIDER_SCOPE (optional) - a list, of strings, of permissions to ask for. 21 | The list of these is `here `_ 22 | 23 | LOG_LEVEL (optional) - A string value containing one of standard python logging 24 | levels of DEBUG, INFO, WARNING, ERROR or CRITICAL. Defaults to "ERROR", which 25 | should be relatively quiet. 26 | 27 | LOG_FILE (optional) - The path to a file that will received appended logging 28 | information. By default, logged messages will print to stdout. 29 | 30 | Example:: 31 | 32 | FACEBOOK_ACCESS_SETTINGS = { 33 | "FACEBOOK_APP_ID": FACEBOOK_APP_ID, 34 | "FACEBOOK_APP_SECRET": FACEBOOK_APP_SECRET, 35 | # The following keys are optional 36 | # "CALLBACK": "la_facebook.callbacks.default.default_facebook_callback", 37 | # "PROVIDER_SCOPE": "email,read_stream", 38 | # "LOG_LEVEL": "DEBUG", 39 | # "LOG_FILE": "/tmp/la_facebook.log", 40 | } 41 | -------------------------------------------------------------------------------- /docs/test_project.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | Running The Test Project 3 | ======================== 4 | 5 | #. Make sure you have Django 1.1 or greater and oauth2 at 1.5.163 or greater installed 6 | #. Change directory to la_facebook/tests/fb_la_test 7 | #. python manage.py syncdb 8 | #. python manage.py runserver localhost:8000 9 | #. Open a browser and point to that URL 10 | #. Manually do the tests 11 | 12 | Log settings 13 | ------------ 14 | 15 | If you don't get enough info from std.out, then go into settings and change the 16 | logging level 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | Get ``django-la-facebook`` into your python path:: 6 | 7 | pip install django-la-facebook 8 | 9 | Add ``la_facebook`` to your INSTALLED_APPS in settings.py:: 10 | 11 | INSTALLED_APPS = ( 12 | ... 13 | 'la_facebook', 14 | ... 15 | ) 16 | 17 | Add ``la_facebook`` to your root urlconf (urls.py):: 18 | 19 | urlpatterns = patterns('', 20 | ..., 21 | url(r"^la_facebook/", include("la_facebook.urls")), 22 | ..., 23 | ) 24 | 25 | TODO: add in the template wiring instructions -------------------------------------------------------------------------------- /la_facebook/__init__.py: -------------------------------------------------------------------------------- 1 | __VERSION__=(0,0,1) -------------------------------------------------------------------------------- /la_facebook/access.py: -------------------------------------------------------------------------------- 1 | import cgi 2 | import datetime 3 | import httplib2 4 | import logging 5 | import socket 6 | import urllib 7 | import urlparse 8 | 9 | from django.conf import settings 10 | from django.core.exceptions import ImproperlyConfigured 11 | from django.core.urlresolvers import reverse 12 | from django.utils import simplejson as json 13 | 14 | from django.contrib.sites.models import Site 15 | 16 | import oauth2 as oauth 17 | 18 | from la_facebook.exceptions import ( 19 | NotAuthorized, MissingToken, UnknownResponse, 20 | ServiceFail, FacebookSettingsKeyError 21 | ) 22 | from la_facebook.models import UserAssociation 23 | from la_facebook.utils.anyetree import etree 24 | from la_facebook.utils.loader import load_path_attr 25 | 26 | 27 | logger = logging.getLogger("la_facebook.access") 28 | 29 | 30 | class OAuthAccess(object): 31 | """ 32 | Authorize OAuth access 33 | """ 34 | 35 | def __init__(self): 36 | self.signature_method = oauth.SignatureMethod_HMAC_SHA1() 37 | self.consumer = oauth.Consumer(self.key, self.secret) 38 | 39 | @property 40 | def key(self): 41 | """ 42 | accessor for key setting 43 | """ 44 | 45 | try: 46 | return settings.FACEBOOK_ACCESS_SETTINGS["FACEBOOK_APP_ID"] 47 | except KeyError: 48 | raise FacebookSettingsKeyError("FACEBOOK_ACCESS_SETTINGS must have a FACEBOOK_APP_ID entry") 49 | 50 | @property 51 | def secret(self): 52 | """ 53 | accessor for secret setting 54 | """ 55 | try: 56 | return settings.FACEBOOK_ACCESS_SETTINGS["FACEBOOK_APP_SECRET"] 57 | except KeyError: 58 | raise FacebookSettingsKeyError("FACEBOOK_ACCESS_SETTINGS must have a FACEBOOK_APP_SECRET entry") 59 | @property 60 | def access_token_url(self): 61 | """ 62 | accessor for access_token setting 63 | """ 64 | return "https://graph.facebook.com/oauth/access_token" 65 | 66 | @property 67 | def authorize_url(self): 68 | """ 69 | accessor for authorize setting 70 | """ 71 | return "https://graph.facebook.com/oauth/authorize" 72 | 73 | @property 74 | def provider_scope(self): 75 | """ 76 | accessor for provider scope setting 77 | """ 78 | try: 79 | return settings.FACEBOOK_ACCESS_SETTINGS["PROVIDER_SCOPE"] 80 | except KeyError: 81 | return None 82 | 83 | def unauthorized_token(self): 84 | """ 85 | This function may handle when a user does not authorize the app to access their facebook information. 86 | """ 87 | #@TODO - Can we delete this? 88 | if not hasattr(self, "_unauthorized_token"): 89 | self._unauthorized_token = self.fetch_unauthorized_token() 90 | return self._unauthorized_token 91 | 92 | def fetch_unauthorized_token(self): 93 | """ 94 | This function may further handle when a user does not authorize the app to access their facebook information. 95 | """ 96 | #@TODO - Can we delete this too? 97 | parameters = { 98 | "oauth_callback": self.callback_url, 99 | } 100 | client = oauth.Client(self.consumer) 101 | response, content = client.request(self.request_token_url, 102 | method = "POST", 103 | # parameters must be urlencoded (which are then re-decoded by 104 | # and re-encoded by oauth2 -- seems lame) 105 | body = urllib.urlencode(parameters), 106 | ) 107 | if response["status"] != "200": 108 | raise UnknownResponse( 109 | "Got %s from %s:\n\n%s" % ( 110 | response["status"], self.request_token_url, content 111 | )) 112 | return oauth.Token.from_string(content) 113 | 114 | @property 115 | def callback_url(self): 116 | """ 117 | current site id 118 | grab base site url 119 | grab callback url 120 | return base and callback url 121 | """ 122 | 123 | current_site = Site.objects.get(pk=settings.SITE_ID) 124 | # @@@ http fix 125 | base_url = "http://%s" % current_site.domain 126 | callback_url = reverse("la_facebook_callback") 127 | return "%s%s" % (base_url, callback_url) 128 | 129 | def authorized_token(self, token, verifier=None): 130 | """ 131 | authorize token 132 | verify OAuth 133 | set OAuth client 134 | post OAuth content 135 | response not 200 raise unknown response 136 | return OAuth token as string 137 | """ 138 | parameters = {} 139 | if verifier: 140 | parameters.update({ 141 | "oauth_verifier": verifier, 142 | }) 143 | client = oauth.Client(self.consumer, token=token) 144 | response, content = client.request(self.access_token_url, 145 | method = "POST", 146 | # parameters must be urlencoded (which are then re-decoded by 147 | # oauth2 -- seems lame) 148 | body = urllib.urlencode(parameters), 149 | ) 150 | if response["status"] != "200": 151 | raise UnknownResponse( 152 | "Got %s from %s:\n\n%s" % ( 153 | response["status"], self.access_token_url, content 154 | )) 155 | return oauth.Token.from_string(content) 156 | 157 | def check_token(self, unauth_token, parameters): 158 | """ 159 | check token 160 | check to see if unauthorized token 161 | if authorized token get code parameters 162 | return OAuth token 163 | """ 164 | if unauth_token: 165 | token = oauth.Token.from_string(unauth_token) 166 | if token.key == parameters.get("oauth_token", "no_token"): 167 | verifier = parameters.get("oauth_verifier") 168 | return self.authorized_token(token, verifier) 169 | else: 170 | return None 171 | else: 172 | code = parameters.get("code") 173 | if code: 174 | params = dict( 175 | client_id = self.key, 176 | redirect_uri = self.callback_url, 177 | ) 178 | params["client_secret"] = self.secret 179 | params["code"] = code 180 | raw_data = urllib.urlopen( 181 | "%s?%s" % ( 182 | self.access_token_url, urllib.urlencode(params) 183 | ) 184 | ).read() 185 | response = cgi.parse_qs(raw_data) 186 | return OAuth20Token( 187 | response["access_token"][-1], 188 | int(response["expires"][-1]) 189 | ) 190 | else: 191 | # @@@ this error case is not nice 192 | return None 193 | 194 | @property 195 | def callback(self): 196 | """ 197 | accessor for callback setting 198 | """ 199 | try: 200 | callback_str = settings.FACEBOOK_ACCESS_SETTINGS["CALLBACK"] 201 | except KeyError: 202 | # raise FacebookSettingsKeyError("FACEBOOK_ACCESS_SETTINGS must have a CALLBACK entry") 203 | callback_str = "la_facebook.callbacks.default.default_facebook_callback" 204 | return load_path_attr(callback_str) 205 | 206 | def authorization_url(self, token=None): 207 | """ 208 | authorization url 209 | if token is none set OAuth params client id, url, set scope and rturn authorization url 210 | else request consumer, token and authorization url 211 | return request authorization url 212 | """ 213 | if token is None: 214 | # OAuth 2.0 215 | params = dict( 216 | client_id = self.key, 217 | redirect_uri = self.callback_url, 218 | ) 219 | scope = self.provider_scope 220 | if scope is not None: 221 | params["scope"] = ",".join(scope) 222 | return self.authorize_url + "?%s" % urllib.urlencode(params) 223 | else: 224 | request = oauth.Request.from_consumer_and_token( 225 | self.consumer, 226 | token = token, 227 | http_url = self.authorize_url, 228 | ) 229 | request.sign_request(self.signature_method, self.consumer, token) 230 | return request.to_url() 231 | 232 | def persist(self, user, token, identifier=None): 233 | """ 234 | set expiration 235 | set defaults 236 | set identifier if not none 237 | if nothing was created save current associated defaults 238 | """ 239 | expires = hasattr(token, "expires") and token.expires or None 240 | defaults = { 241 | "token": str(token), 242 | "expires": expires, 243 | } 244 | if identifier is not None: 245 | defaults["identifier"] = identifier 246 | assoc, created = UserAssociation.objects.get_or_create( 247 | user = user, 248 | defaults = defaults, 249 | ) 250 | if not created: 251 | assoc.token = str(token) 252 | assoc.expires = expires 253 | assoc.save() 254 | 255 | def lookup_user(self, identifier): 256 | """ 257 | query all users 258 | query select user 259 | try identify user 260 | if user does not exist return none 261 | else return user 262 | """ 263 | queryset = UserAssociation.objects.all() 264 | queryset = queryset.select_related("user") 265 | try: 266 | assoc = queryset.get(identifier=identifier) 267 | except UserAssociation.DoesNotExist: 268 | return None 269 | else: 270 | return assoc.user 271 | 272 | def make_api_call(self, kind, url, token, method="GET", **kwargs): 273 | if isinstance(token, OAuth20Token): 274 | request_kwargs = dict(method=method) 275 | if method == "POST": 276 | params = { 277 | "access_token": str(token), 278 | } 279 | params.update(kwargs["params"]) 280 | request_kwargs["body"] = urllib.urlencode(params) 281 | else: 282 | url += "?%s" % urllib.urlencode(dict(access_token=str(token))) 283 | http = httplib2.Http() 284 | response, content = http.request(url, **request_kwargs) 285 | else: 286 | if isinstance(token, basestring): 287 | token = oauth.Token.from_string(token) 288 | client = Client(self.consumer, token=token) 289 | # @@@ LinkedIn requires Authorization header which is supported in 290 | # sub-classed version of Client (originally from oauth2) 291 | request_kwargs = dict(method=method, force_auth_header=True) 292 | if method == "POST": 293 | request_kwargs["body"] = urllib.urlencode(kwargs["params"]) 294 | response, content = client.request(url, **request_kwargs) 295 | if response["status"] == "401": 296 | raise NotAuthorized() 297 | if not content: 298 | raise ServiceFail("no content") 299 | logger.debug("response: %r" % response) 300 | logger.debug("content: %r" % content) 301 | if kind == "raw": 302 | return content 303 | elif kind == "json": 304 | try: 305 | return json.loads(content) 306 | except ValueError: 307 | # @@@ might be better to return a uniform cannot parse 308 | # exception and let caller determine if it is service fail 309 | raise ServiceFail("JSON parse error") 310 | elif kind == "xml": 311 | return etree.ElementTree(etree.fromstring(content)) 312 | else: 313 | raise Exception("unsupported API kind") 314 | 315 | 316 | class Client(oauth.Client): 317 | """ 318 | Custom client to support forcing Authorization header (which is required 319 | by LinkedIn). See http://github.com/brosner/python-oauth2/commit/82a05f96878f187f67c1af44befc1bec562e5c1f 320 | """ 321 | 322 | def request(self, uri, method="GET", body=None, headers=None, 323 | redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None, 324 | force_auth_header=False): 325 | 326 | DEFAULT_CONTENT_TYPE = "application/x-www-form-urlencoded" 327 | 328 | if not isinstance(headers, dict): 329 | headers = {} 330 | 331 | is_multipart = method == "POST" and headers.get("Content-Type", DEFAULT_CONTENT_TYPE) != DEFAULT_CONTENT_TYPE 332 | 333 | if body and method == "POST" and not is_multipart: 334 | parameters = dict(urlparse.parse_qsl(body)) 335 | else: 336 | parameters = None 337 | 338 | req = oauth.Request.from_consumer_and_token(self.consumer, 339 | token=self.token, http_method=method, http_url=uri, 340 | parameters=parameters) 341 | 342 | req.sign_request(self.method, self.consumer, self.token) 343 | 344 | if force_auth_header: 345 | headers.update(req.to_header()) 346 | 347 | if method == "POST": 348 | headers["Content-Type"] = headers.get("Content-Type", DEFAULT_CONTENT_TYPE) 349 | if is_multipart: 350 | headers.update(req.to_header()) 351 | else: 352 | if not force_auth_header: 353 | body = req.to_postdata() 354 | else: 355 | body = urllib.urlencode(req.get_nonoauth_parameters(), True) 356 | elif method == "GET": 357 | if not force_auth_header: 358 | uri = req.to_url() 359 | else: 360 | if not force_auth_header: 361 | # don't call update twice. 362 | headers.update(req.to_header()) 363 | 364 | return httplib2.Http.request(self, uri, method=method, body=body, 365 | headers=headers, redirections=redirections, 366 | connection_type=connection_type) 367 | 368 | 369 | class OAuth20Token(object): 370 | 371 | def __init__(self, token, expires=None): 372 | self.token = token 373 | if expires is not None: 374 | self.expires = datetime.datetime.now() + datetime.timedelta(seconds=expires) 375 | else: 376 | self.expires = None 377 | 378 | def __str__(self): 379 | return str(self.token) 380 | -------------------------------------------------------------------------------- /la_facebook/callbacks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydanny/django-la-facebook/790efe257c1265df8eefad2f0749f1cc3bb8a328/la_facebook/callbacks/__init__.py -------------------------------------------------------------------------------- /la_facebook/callbacks/base.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.http import HttpResponse 3 | from django.shortcuts import redirect 4 | from django.db.models import get_model 5 | 6 | from django.contrib.auth import login 7 | from django.contrib.auth.models import User 8 | 9 | from django.conf import settings 10 | 11 | from la_facebook.la_fb_logging import logger 12 | 13 | LA_FACEBOOK_PROFILE_PREFIX = 'fb-' 14 | FACEBOOK_GRAPH_TARGET = "https://graph.facebook.com/me" 15 | 16 | 17 | def get_default_redirect(request, fallback_url=settings.LOGIN_REDIRECT_URL, redirect_field_name="next", session_key_value="redirect_to"): 18 | """ 19 | Returns the URL to be used in login procedures by looking at different 20 | values in the following order: 21 | 22 | - a REQUEST value, GET or POST, named "next" by default. 23 | - LOGIN_REDIRECT_URL - the URL in the setting 24 | - LOGIN_REDIRECT_URLNAME - the name of a URLconf entry in the settings 25 | """ 26 | 27 | redirect_to = request.REQUEST.get(redirect_field_name) 28 | if not redirect_to: 29 | # try the session if available 30 | if hasattr(request, "session"): 31 | redirect_to = request.session.get(session_key_value) 32 | # light security check -- make sure redirect_to isn't garabage. 33 | if not redirect_to or "://" in redirect_to or " " in redirect_to: 34 | redirect_to = fallback_url 35 | return redirect_to 36 | 37 | 38 | class BaseFacebookCallback(object): 39 | 40 | def __call__(self, request, access, token): 41 | if not request.user.is_authenticated(): 42 | logger.debug("BaseFacebookCallback.__call__: request.user not authenticated") 43 | authenticated = False 44 | user_data = self.fetch_user_data(request, access, token) 45 | user = self.lookup_user(request, access, user_data) 46 | if user is None: 47 | logger.debug("BaseFacebookCallback.__call__: no existing django user found for this facebook identifier") 48 | ret = self.handle_no_user(request, access, token, user_data) 49 | # allow handle_no_user to create a user if need be 50 | if isinstance(ret, User): 51 | logger.debug("BaseFacebookCallback.__call__: self.handle_no_user returned valid django user") 52 | user = ret 53 | else: 54 | logger.debug("BaseFacebookCallback.__call__: existing django user found for this facebook identifier") 55 | ret = self.handle_unauthenticated_user(request, user, access, token, user_data) 56 | if isinstance(ret, HttpResponse): 57 | return ret 58 | else: 59 | logger.debug("BaseFacebookCallback.__call__: request.user is authenticated") 60 | authenticated = True 61 | user = request.user 62 | redirect_to = self.redirect_url(request) 63 | if user: 64 | kwargs = {} 65 | if not authenticated: 66 | kwargs["identifier"] = self.identifier_from_data(user_data) 67 | logger.debug("BaseFacebookCallback.__call__: Persisting user token") 68 | access.persist(user, token, **kwargs) 69 | 70 | return redirect(redirect_to) 71 | 72 | def fetch_user_data(self, request, access, token): 73 | raise NotImplementedError("Callbacks must have a fetch_user_data method") 74 | 75 | def lookup_user(self, request, access, user_data): 76 | raise NotImplementedError("Callbacks must have a lookup_user method") 77 | 78 | def redirect_url(self, request): 79 | raise NotImplementedError("Callbacks must have a redirect_url method") 80 | 81 | def handle_no_user(self, request, access, token, user_data): 82 | raise NotImplementedError("Callbacks must have a handle_no_user method") 83 | 84 | def handle_unauthenticated_user(self, request, user, access, token, user_data): 85 | raise NotImplementedError("Callbacks must have a handle_unauthenticated_user method") 86 | 87 | def update_profile_from_graph(self, request, access, token, profile): 88 | raise NotImplementedError("Callbacks must have a update_profile_from_graph method") 89 | 90 | def create_profile(self, request, access, token, user): 91 | raise NotImplementedError("Callbacks must have a create_profile method") 92 | 93 | def create_user(self, request, access, token, user_data): 94 | raise NotImplementedError("Callbacks must have a create_user method") 95 | 96 | def identifier_from_data(self, data): 97 | # @@@ currently this is being used to make/lookup users and we don't 98 | # want a clash between services. need to look into the more. 99 | return LA_FACEBOOK_PROFILE_PREFIX + data["id"] 100 | 101 | def login_user(self, request, user): 102 | user.backend = "django.contrib.auth.backends.ModelBackend" 103 | logger.debug("BaseFacebookCallback.login_user: logging in user %s" \ 104 | "with ModelBackend" % str(user).strip()) 105 | login(request, user) 106 | -------------------------------------------------------------------------------- /la_facebook/callbacks/default.py: -------------------------------------------------------------------------------- 1 | from django.db.models import get_model 2 | 3 | from django.contrib.auth.models import User 4 | 5 | from django.conf import settings 6 | 7 | from la_facebook.la_fb_logging import logger 8 | from la_facebook.callbacks.base import (BaseFacebookCallback, 9 | get_default_redirect, FACEBOOK_GRAPH_TARGET) 10 | 11 | class DefaultFacebookCallback(BaseFacebookCallback): 12 | 13 | def fetch_user_data(self, request, access, token): 14 | url = FACEBOOK_GRAPH_TARGET 15 | return access.make_api_call("json", url, token) 16 | 17 | def lookup_user(self, request, access, user_data): 18 | return access.lookup_user(identifier=self.identifier_from_data(user_data)) 19 | 20 | def redirect_url(self, request): 21 | return get_default_redirect(request) 22 | 23 | def handle_no_user(self, request, access, token, user_data): 24 | return self.create_user(request, access, token, user_data) 25 | 26 | def handle_unauthenticated_user(self, request, user, access, token, user_data): 27 | self.login_user(request, user) 28 | 29 | def update_profile_from_graph(self, request, access, token, profile): 30 | user_data = self.fetch_user_data(request, access, token) 31 | for k, v in user_data.items(): 32 | if k !='id' and hasattr(profile, k): 33 | setattr(profile, k, v) 34 | logger.debug("DefaultFacebookCallback.update_profile_from_graph"\ 35 | ": updating profile %s to %s" % (k,v)) 36 | return profile 37 | 38 | def create_profile(self, request, access, token, user): 39 | 40 | if hasattr(settings, 'AUTH_PROFILE_MODULE'): 41 | profile_model = get_model(*settings.AUTH_PROFILE_MODULE.split('.')) 42 | 43 | profile, created = profile_model.objects.get_or_create( 44 | user = user, 45 | ) 46 | profile = self.update_profile_from_graph(request, access, token, profile) 47 | profile.save() 48 | 49 | else: 50 | # Do nothing because users have no site profile defined 51 | # TODO - should we pass a warning message? Raise a SiteProfileNotAvailable error? 52 | logger.warning("DefaultFacebookCallback.create_profile: unable to" \ 53 | "create/update profile as no AUTH_PROFILE_MODULE setting" \ 54 | "has been defined") 55 | pass 56 | 57 | def create_user(self, request, access, token, user_data): 58 | identifier = self.identifier_from_data(user_data) 59 | username = str(identifier) 60 | if User.objects.filter(username=username).count(): 61 | logger.warning("DefaultFacebookCallback.create_user: A user for" \ 62 | "was already found, when asked to create a user for %s" 63 | % username) 64 | user = User.objects.get(username=username) 65 | else: 66 | user = User(username=str(identifier)) 67 | user.set_unusable_password() 68 | user.save() 69 | logger.debug("DefaultFacebookCallback.create_user: new django" \ 70 | "user created for %s" % username) 71 | 72 | self.create_profile(request, access, token, user) 73 | 74 | self.login_user(request, user) 75 | return user 76 | 77 | default_facebook_callback = DefaultFacebookCallback() 78 | -------------------------------------------------------------------------------- /la_facebook/exceptions.py: -------------------------------------------------------------------------------- 1 | class FacebookSettingsKeyError(KeyError): 2 | """ Expect to see at least: 3 | 4 | FACEBOOK_ACCESS_SETTINGS = { 5 | "keys": { 6 | "FACEBOOK_APP_ID": , 7 | "FACEBOOK_APP_SECRET": , 8 | }, 9 | "endpoints": { 10 | # Will need to redo the following 11 | "callback": "fb_la_test.oauth.facebook_callback", 12 | # Probably too much power here - just need to have authentication 13 | } 14 | } 15 | """ 16 | pass 17 | 18 | 19 | class NotAuthorized(Exception): 20 | pass 21 | 22 | 23 | class MissingToken(Exception): 24 | pass 25 | 26 | 27 | class UnknownResponse(Exception): 28 | pass 29 | 30 | 31 | class ServiceFail(Exception): 32 | pass 33 | -------------------------------------------------------------------------------- /la_facebook/la_fb_logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from django.conf import settings 4 | 5 | logger = logging.getLogger("la_fb") 6 | 7 | 8 | try: 9 | log_level = settings.FACEBOOK_ACCESS_SETTINGS['LOG_LEVEL'] 10 | except (KeyError, AttributeError): 11 | log_level = 'CRITICAL' 12 | 13 | log_level = log_level.upper() #just to catch mistakenly entered lowercase 14 | if hasattr(logging, log_level): 15 | logger.setLevel(getattr(logging, log_level)) 16 | 17 | # TODO: should we always log to console, doesn't seem worth another setting 18 | logger.addHandler(logging.StreamHandler(sys.stdout)) 19 | 20 | try: 21 | log_file = settings.FACEBOOK_ACCESS_SETTINGS['LOG_FILE'] 22 | logger.addHandler(logging.FileHandler(log_file)) 23 | except (KeyError, AttributeError): 24 | pass 25 | -------------------------------------------------------------------------------- /la_facebook/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.db import models 4 | 5 | from django.contrib.auth.models import User 6 | 7 | 8 | class UserAssociation(models.Model): 9 | 10 | user = models.ForeignKey(User, unique=True) 11 | identifier = models.CharField(max_length=255, db_index=True) 12 | token = models.CharField(max_length=200) 13 | expires = models.DateTimeField(null=True) 14 | 15 | def expired(self): 16 | return datetime.datetime.now() < self.expires 17 | -------------------------------------------------------------------------------- /la_facebook/templates/la_facebook/fb_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Facebook Error 5 | 6 | 7 |

Facebook Error

8 | {% if error %} 9 |

OAuth Error: {{error}}

10 | {% endif %} 11 | {% if fb_error %} 12 |

Facebook Error: {{fb_error}}

13 | {% endif %} 14 | {% if fb_error_reason%} 15 |

Reason: {{fb_error_reason}}

16 | {% endif %} 17 | {% if fb_error_description %} 18 |

Description: {{fb_error_description}}

19 | {% endif %} 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /la_facebook/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydanny/django-la-facebook/790efe257c1265df8eefad2f0749f1cc3bb8a328/la_facebook/templatetags/__init__.py -------------------------------------------------------------------------------- /la_facebook/templatetags/la_facebook_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from la_facebook.models import UserAssociation 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.filter 9 | def authed_via(user): 10 | if user.is_authenticated(): 11 | try: 12 | assoc = UserAssociation.objects.get(user=user) 13 | except UserAssociation.DoesNotExist: 14 | return False 15 | return assoc.expired() 16 | else: 17 | return False 18 | -------------------------------------------------------------------------------- /la_facebook/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from la_facebook.tests.test_access_module import * -------------------------------------------------------------------------------- /la_facebook/tests/test_access_module.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.test import TestCase 3 | from django.contrib.sites.models import Site 4 | from django.core.urlresolvers import reverse 5 | from la_facebook.access import OAuthAccess 6 | from la_facebook.utils.loader import load_path_attr 7 | 8 | class PropertyTests(TestCase): 9 | 10 | def test_key_in_settings(self): 11 | # test if there is a key 12 | oauth = OAuthAccess() 13 | self.assertEquals(oauth.key, "124397597633470") 14 | 15 | def test_secret_in_settings(self): 16 | oauth = OAuthAccess() 17 | self.assertEquals(oauth.secret, "cdd60917e6a30548b933ba91c48289bc") 18 | 19 | def test_access_token_url(self): 20 | oauth = OAuthAccess() 21 | access_token_endpoint = oauth.access_token_url 22 | expected_endpoints_url = "https://graph.facebook.com/oauth/access_token" 23 | self.assertEquals(access_token_endpoint,expected_endpoints_url) 24 | 25 | def test_authorize_url(self): 26 | oauth = OAuthAccess() 27 | authorize_url_endpoint = oauth.authorize_url 28 | expected_endpoint_url = "https://graph.facebook.com/oauth/authorize" 29 | self.assertEquals(authorize_url_endpoint,expected_endpoint_url) 30 | 31 | def test_provider_scope(self): 32 | oauth = OAuthAccess() 33 | provider_scope_endpoint = oauth.provider_scope 34 | expected_endpoint_url = None 35 | self.assertEquals(provider_scope_endpoint,expected_endpoint_url) 36 | 37 | def test_callback_url(self): 38 | oauth = OAuthAccess() 39 | callback_url = oauth.callback_url 40 | current_site = Site.objects.get(pk=settings.SITE_ID) 41 | base_url = "http://%s" % current_site.domain 42 | reversed_url = reverse("la_facebook_callback") 43 | expected_url = "%s%s" % (base_url, reversed_url) 44 | self.assertEquals(callback_url,expected_url) 45 | 46 | def test_callback(self): 47 | oauth = OAuthAccess() 48 | callback_endpoint = oauth.callback 49 | expected_callback_endpoint = load_path_attr(settings.FACEBOOK_ACCESS_SETTINGS["CALLBACK"]) 50 | self.assertEquals(callback_endpoint,expected_callback_endpoint) 51 | -------------------------------------------------------------------------------- /la_facebook/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | 4 | urlpatterns = patterns("la_facebook.views", 5 | url( 6 | regex = r"^login/?$", 7 | view = "facebook_login", 8 | name = "la_facebook_login", 9 | ), 10 | url( 11 | regex = r"^callback/?$", 12 | view = "facebook_callback", 13 | name = "la_facebook_callback" 14 | ), 15 | url( 16 | regex = r"^finish_signup/?$", 17 | view = "finish_signup", 18 | name = "la_facebook_finish_signup" 19 | ) 20 | ) -------------------------------------------------------------------------------- /la_facebook/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydanny/django-la-facebook/790efe257c1265df8eefad2f0749f1cc3bb8a328/la_facebook/utils/__init__.py -------------------------------------------------------------------------------- /la_facebook/utils/anyetree.py: -------------------------------------------------------------------------------- 1 | """ 2 | Get an Etree library. Usage:: 3 | 4 | >>> from anyetree import etree 5 | 6 | Returns some etree library. Looks for (in order of decreasing preference): 7 | 8 | * ``lxml.etree`` (http://cheeseshop.python.org/pypi/lxml/) 9 | * ``xml.etree.cElementTree`` (built into Python 2.5) 10 | * ``cElementTree`` (http://effbot.org/zone/celementtree.htm) 11 | * ``xml.etree.ElementTree`` (built into Python 2.5) 12 | * ``elementree.ElementTree (http://effbot.org/zone/element-index.htm) 13 | """ 14 | 15 | 16 | __all__ = ["etree"] 17 | 18 | 19 | SEARCH_PATHS = [ 20 | "lxml.etree", 21 | "xml.etree.cElementTree", 22 | "cElementTree", 23 | "xml.etree.ElementTree", 24 | "elementtree.ElementTree", 25 | ] 26 | 27 | etree = None 28 | 29 | for name in SEARCH_PATHS: 30 | try: 31 | # @@@ move to import_module 32 | etree = __import__(name, {}, {}, [""]) 33 | break 34 | except ImportError: 35 | continue 36 | 37 | if etree is None: 38 | raise ImportError("No suitable ElementTree implementation found.") -------------------------------------------------------------------------------- /la_facebook/utils/loader.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ImproperlyConfigured 2 | try: 3 | from django.utils.importlib import import_module 4 | except ImportError: 5 | from importlib import import_module 6 | 7 | 8 | 9 | def load_path_attr(path): 10 | i = path.rfind(".") 11 | module, attr = path[:i], path[i+1:] 12 | try: 13 | mod = import_module(module) 14 | except ImportError, e: 15 | raise ImproperlyConfigured("Error importing %s: '%s'" % (module, e)) 16 | try: 17 | attr = getattr(mod, attr) 18 | except AttributeError, e: 19 | raise ImproperlyConfigured("Module '%s' does not define a '%s %s'" % (module, attr, e)) 20 | return attr 21 | -------------------------------------------------------------------------------- /la_facebook/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseRedirect 2 | from django.shortcuts import render_to_response 3 | from django.template import RequestContext 4 | 5 | from la_facebook.access import OAuthAccess 6 | from la_facebook.exceptions import MissingToken 7 | from la_facebook.la_fb_logging import logger 8 | 9 | 10 | def facebook_login(request, redirect_field_name="next", 11 | redirect_to_session_key="redirect_to"): 12 | """ 13 | 1. access OAuth 14 | 2. set token to none 15 | 3. store and redirect to authorization url 16 | 4. redirect to OAuth authorization url 17 | """ 18 | 19 | access = OAuthAccess() 20 | token = None 21 | if hasattr(request, "session"): 22 | logger.debug("la_facebook.views.facebook_login: request has session") 23 | request.session[redirect_to_session_key] = request.GET.get(redirect_field_name) 24 | return HttpResponseRedirect(access.authorization_url(token)) 25 | 26 | 27 | def facebook_callback(request): 28 | """ 29 | 1. define RequestContext 30 | 2. access OAuth 31 | 3. check session 32 | 4. autheticate token 33 | 5. raise exception if missing token 34 | 6. return access callback 35 | 7. raise exception if mismatch token 36 | 8. render error 37 | """ 38 | 39 | ctx = RequestContext(request) 40 | access = OAuthAccess() 41 | # TODO: Check to make sure the session cookie is setting correctly 42 | unauth_token = request.session.get("unauth_token", None) 43 | try: 44 | auth_token = access.check_token(unauth_token, request.GET) 45 | except MissingToken: 46 | ctx.update({"error": "token_missing"}) 47 | logger.error('la_facebook.views.facebook_login: missing token') 48 | else: 49 | if auth_token: 50 | return access.callback(request, access, auth_token) 51 | else: 52 | # @@@ not nice for OAuth 2 53 | ctx.update({"error": "token_mismatch"}) 54 | logger.error('la_facebook.views.facebook_callback: token mismatch'\ 55 | ', error getting token, or user denied FB login') 56 | 57 | # we either have a missing token or a token mismatch 58 | # Facebook provides some error details in the callback URL 59 | fb_errors = [] 60 | for fb_error_detail in ['error', 'error_description', 'error_reason']: 61 | if fb_error_detail in request.GET: 62 | ctx['fb_' + fb_error_detail] = request.GET[fb_error_detail] 63 | fb_errors.append(request.GET[fb_error_detail]) 64 | 65 | logger.warning('la_facebook.views.facebook_callback: %s' 66 | % ', '.join(fb_errors)) 67 | 68 | return render_to_response("la_facebook/fb_error.html", ctx) 69 | 70 | 71 | def finish_signup(request): 72 | """ 73 | 1. access OAuth 74 | 2. return callback url and finish signup 75 | """ 76 | 77 | access = OAuthAccess() 78 | return access.callback.finish_signup(request) 79 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | oauth2==1.5.163 2 | django==1.2.4 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = docs/ 3 | build-dir = docs/_build 4 | all_files = 1 5 | 6 | [upload_sphinx] 7 | upload-dir = docs/_build/html 8 | docs-dir = docs/ 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name = "django-la-facebook", 5 | version = "0.1.alpha", 6 | author = "pydanny", 7 | author_email = "pydanny@pydanny.com", 8 | description = "Definitive facebook auth for Django", 9 | long_description = open("README.rst").read(), 10 | license = "BSD", 11 | url = "http://github.com/pydanny/django-la-facebook", 12 | packages = [ 13 | "la_facebook", 14 | "la_facebook.templatetags", 15 | "la_facebook.utils", 16 | ], 17 | classifiers = [ 18 | "Development Status :: 3 - Alpha", 19 | "Environment :: Web Environment", 20 | "Intended Audience :: Developers", 21 | "License :: OSI Approved :: BSD License", 22 | "Operating System :: OS Independent", 23 | "Programming Language :: Python", 24 | "Framework :: Django", 25 | ] 26 | ) -------------------------------------------------------------------------------- /test_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydanny/django-la-facebook/790efe257c1265df8eefad2f0749f1cc3bb8a328/test_project/__init__.py -------------------------------------------------------------------------------- /test_project/connect/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydanny/django-la-facebook/790efe257c1265df8eefad2f0749f1cc3bb8a328/test_project/connect/__init__.py -------------------------------------------------------------------------------- /test_project/connect/fixtures/initial_data.json: -------------------------------------------------------------------------------- 1 | [{"pk": 1, "model": "sites.site", "fields": {"domain": "localhost:8000", "name": "localhost"}}] -------------------------------------------------------------------------------- /test_project/connect/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | 4 | # Create your models here. 5 | 6 | class Profile(models.Model): 7 | user = models.ForeignKey(User) 8 | bio = models.TextField() 9 | name = models.CharField(max_length=255) -------------------------------------------------------------------------------- /test_project/connect/templates/after.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This is a test 5 | 6 | 7 | 8 | {% if user %} 9 | {% if profile %} 10 | Your name is {{ profile.name }}. You're all about {{ profile.bio }} 11 | {% else %} 12 | You're logged in as {{ user.username }}, but you don't seem to have a profile. 13 | {% endif %} 14 | {% else %} 15 | You don't seem to be logged in =( 16 | {% endif %} 17 | 18 | -------------------------------------------------------------------------------- /test_project/connect/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This is a test 5 | 6 | 7 | Connect to Facebook! 8 | 9 | -------------------------------------------------------------------------------- /test_project/connect/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from fb_la_test.connect.tests.test_connect import * 2 | -------------------------------------------------------------------------------- /test_project/connect/tests/test_connect.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | import httplib2 4 | 5 | from fb_la_test.connect.tests.utils import LaFacebookTestCase 6 | 7 | class TestConnection(LaFacebookTestCase): 8 | 9 | 10 | def test_login_first_stage(self): 11 | """ This test checks that we can touch facebook's oauth client""" 12 | 13 | # based off http://httplib2.googlecode.com/hg/doc/html 14 | 15 | url = "https://www.facebook.com/dialog/oauth?client_id=%s&redirect_uri=http%%3A//localhost%%3A8000/after&scope=email,read_stream" % settings.FACEBOOK_APP_ID 16 | h = httplib2.Http() 17 | resp, content = h.request(url, method="GET") 18 | 19 | form_data = dict( 20 | charset_test=unicode(r"€,´,\xe2,\xc2,\xd0,\xd0", "utf-8"), 21 | lsd="IuLli", 22 | next="http://www.facebook.com/connect/uiserver.php?method=permissions.request&app_id=124397597633470&display=page&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fafter&response_type=code&fbconnect=1&perms=email%2Cread_stream&from_login=1", 23 | api_key="124397597633470", 24 | return_session="0", 25 | cancel_url="http://localhost:8000/after?error_reason=user_denied&error=access_denied&error_description=The+user+denied+your+request.", 26 | legacy_return="1", 27 | display="page", 28 | session_key_only="0", 29 | skip_api_login="1", 30 | trynum="1", 31 | email="", 32 | pass1="", 33 | persistent="1", 34 | default_persistent="0", 35 | Login="login" 36 | ) 37 | form_data["pass"] = form_data["pass1"] 38 | del form_data["pass1"] 39 | 40 | 41 | self.assertRaises(TypeError, 42 | h.request, 43 | "http://www.facebook.com/connect/uiserver.php", 44 | body=form_data 45 | ) 46 | 47 | def test_authentication_after_facebook_authentication(self): 48 | pass 49 | -------------------------------------------------------------------------------- /test_project/connect/tests/utils.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | class LaFacebookTestCase(TestCase): 4 | 5 | def response_200(self, response): 6 | self.assertEqual(response.status_code, 200) 7 | 8 | def response_302(self, response): 9 | self.assertEqual(response.status_code, 302) 10 | 11 | 12 | -------------------------------------------------------------------------------- /test_project/connect/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns('test_project.connect.views', 4 | url(r'^/?$', "test_index", name="index"), 5 | url(r'^after/?$', "after", name="after"), 6 | ) -------------------------------------------------------------------------------- /test_project/connect/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.shortcuts import render_to_response 3 | from test_project.connect.models import Profile 4 | 5 | def test_index(request): 6 | context_dict = { 7 | 'app_id': settings.FACEBOOK_APP_ID, 8 | } 9 | 10 | return render_to_response('index.html', context_dict) 11 | 12 | def after(request): 13 | # Let's prove facebook's creepy stalker-ware is working 14 | # TODO: Needs a lot of validation 15 | context_dict = {} 16 | 17 | if hasattr(request, 'user'): 18 | context_dict['user'] = request.user 19 | try: 20 | context_dict['profile'] = request.user.get_profile() 21 | except Profile.DoesNotExist: 22 | pass 23 | 24 | 25 | return render_to_response('after.html', context_dict) -------------------------------------------------------------------------------- /test_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | 4 | try: 5 | import settings # Assumed to be in the same directory. 6 | except ImportError: 7 | import sys 8 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 9 | sys.exit(1) 10 | 11 | if __name__ == "__main__": 12 | execute_manager(settings) 13 | -------------------------------------------------------------------------------- /test_project/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for fb_la_test project. 2 | import sys, os 3 | 4 | # We need the project's root in our Python Path. Let's add it 5 | test_project = os.path.dirname(__file__) 6 | test_directory = os.path.dirname(test_project) 7 | sys.path.append(test_directory) 8 | 9 | DEBUG = True 10 | TEMPLATE_DEBUG = DEBUG 11 | 12 | ADMINS = ( 13 | # ('Your Name', 'your_email@domain.com'), 14 | ) 15 | 16 | MANAGERS = ADMINS 17 | 18 | DATABASES = { 19 | 'default': { 20 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 21 | 'NAME': 'dummy', # Or path to database file if using sqlite3. 22 | 'USER': '', # Not used with sqlite3. 23 | 'PASSWORD': '', # Not used with sqlite3. 24 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 25 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 26 | } 27 | } 28 | 29 | # Local time zone for this installation. Choices can be found here: 30 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 31 | # although not all choices may be available on all operating systems. 32 | # On Unix systems, a value of None will cause Django to use the same 33 | # timezone as the operating system. 34 | # If running in a Windows environment this must be set to the same as your 35 | # system time zone. 36 | TIME_ZONE = 'America/Chicago' 37 | 38 | # Language code for this installation. All choices can be found here: 39 | # http://www.i18nguy.com/unicode/language-identifiers.html 40 | LANGUAGE_CODE = 'en-us' 41 | 42 | SITE_ID = 1 43 | 44 | # If you set this to False, Django will make some optimizations so as not 45 | # to load the internationalization machinery. 46 | USE_I18N = True 47 | 48 | # If you set this to False, Django will not format dates, numbers and 49 | # calendars according to the current locale 50 | USE_L10N = True 51 | 52 | # Absolute filesystem path to the directory that will hold user-uploaded files. 53 | # Example: "/home/media/media.lawrence.com/" 54 | MEDIA_ROOT = '' 55 | 56 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 57 | # trailing slash if there is a path component (optional in other cases). 58 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 59 | MEDIA_URL = '' 60 | 61 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 62 | # trailing slash. 63 | # Examples: "http://foo.com/media/", "/media/". 64 | ADMIN_MEDIA_PREFIX = '/media/' 65 | 66 | # Make this unique, and don't share it with anybody. 67 | SECRET_KEY = 'hdik5*)66ahwns+q4ckb@7cehrq=n^e^^y23*sm(xn-f785b7b' 68 | 69 | # List of callables that know how to import templates from various sources. 70 | TEMPLATE_LOADERS = ( 71 | 'django.template.loaders.filesystem.Loader', 72 | 'django.template.loaders.app_directories.Loader', 73 | # 'django.template.loaders.eggs.Loader', 74 | ) 75 | 76 | MIDDLEWARE_CLASSES = ( 77 | 'django.middleware.common.CommonMiddleware', 78 | 'django.contrib.sessions.middleware.SessionMiddleware', 79 | 'django.middleware.csrf.CsrfViewMiddleware', 80 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 81 | 'django.contrib.messages.middleware.MessageMiddleware', 82 | ) 83 | 84 | ROOT_URLCONF = 'test_project.urls' 85 | 86 | TEMPLATE_DIRS = ( 87 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 88 | # Always use forward slashes, even on Windows. 89 | # Don't forget to use absolute paths, not relative paths. 90 | ) 91 | 92 | INSTALLED_APPS = ( 93 | 'django.contrib.auth', 94 | 'django.contrib.admin', 95 | 'django.contrib.contenttypes', 96 | 'django.contrib.sessions', 97 | 'django.contrib.sites', 98 | 'django.contrib.messages', 99 | 'test_project.connect', 100 | 'la_facebook', 101 | ) 102 | 103 | FACEBOOK_APP_ID = '124397597633470' 104 | FACEBOOK_API_KEY = '0d6acba060823bac2f93708d98d7e74a' 105 | FACEBOOK_APP_SECRET = 'cdd60917e6a30548b933ba91c48289bc' 106 | 107 | AUTH_PROFILE_MODULE="connect.Profile" 108 | 109 | 110 | FACEBOOK_ACCESS_SETTINGS = { 111 | "FACEBOOK_APP_ID": FACEBOOK_APP_ID, 112 | "FACEBOOK_APP_SECRET": FACEBOOK_APP_SECRET, 113 | "LOG_LEVEL": "DEBUG", 114 | "LOG_FILE": "/tmp/la_facebook.log", 115 | # The following keys are optional 116 | # "CALLBACK": "la_facebook.callbacks.default.default_facebook_callback", 117 | #"PROVIDER_SCOPE": ['email','read_stream'], # here as sample - optional 118 | } 119 | -------------------------------------------------------------------------------- /test_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from django.contrib import admin 3 | admin.autodiscover() 4 | urlpatterns = patterns('', 5 | url(r'^', include('test_project.connect.urls')), 6 | url(r"^la_facebook/", include("la_facebook.urls")), 7 | url(r'^admin/(.*)', admin.site.root) 8 | ) 9 | --------------------------------------------------------------------------------