├── .gitignore ├── AUTHORS ├── Changelog ├── LICENSE ├── MANIFEST.in ├── README ├── TODO ├── bootstrap.py ├── buildout.cfg ├── chishop ├── __init__.py ├── conf │ ├── __init__.py │ └── default.py ├── manage.py ├── media │ ├── __init__.py │ ├── dists │ │ └── __init__.py │ └── style │ │ └── djangopypi.css ├── production_example.py ├── settings.py ├── templates │ ├── 404.html │ ├── 500.html │ ├── admin │ │ └── base_site.html │ ├── base.html │ ├── base_site.html │ ├── djangopypi │ │ ├── pypi.html │ │ ├── pypi_show_links.html │ │ ├── search.html │ │ ├── search_results.html │ │ ├── show_links.html │ │ ├── show_version.html │ │ └── simple.html │ └── registration │ │ ├── activate.html │ │ ├── activation_complete.html │ │ ├── activation_email.txt │ │ ├── activation_email_subject.txt │ │ ├── login.html │ │ ├── logout.html │ │ ├── registration_closed.html │ │ ├── registration_complete.html │ │ └── registration_form.html └── urls.py ├── djangopypi ├── __init__.py ├── admin.py ├── forms.py ├── http.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── loadclassifiers.py │ │ └── ppadd.py ├── models.py ├── templatetags │ ├── __init__.py │ └── safemarkup.py ├── tests.py ├── urls.py ├── utils.py └── views │ ├── __init__.py │ ├── dists.py │ ├── search.py │ └── users.py ├── index.html └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *~ 4 | *.sqlite 5 | *.sqlite-journal 6 | settings_local.py 7 | .*.sw[po] 8 | *.kpf 9 | dist/ 10 | *.egg-info 11 | doc/__build/* 12 | pip-log.txt 13 | devdatabase.db 14 | parts 15 | eggs 16 | bin 17 | developer-eggs 18 | downloads 19 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Ask Solem 2 | Rune Halvorsen 3 | Russell Sim 4 | Brian Rosner 5 | Hugo Lopes Tavares 6 | Sverre Johansen 7 | Bo Shi 8 | Carl Meyer 9 | Vinícius das Chagas Silva 10 | Vanderson Mota dos Santos 11 | Stefan Foulis 12 | Michael Richardson 13 | Halldór Rúnarsson 14 | Brent Tubbs 15 | David Cramer 16 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | ============== 2 | Change history 3 | ============== 4 | 5 | 0.2.0 :date:`2009-03-22 1:21 A.M CET` :author:askh@opera.com 6 | -------------------------------------------------------------- 7 | 8 | Backwards incompatible changes 9 | ------------------------------- 10 | 11 | * Projects now has an associated owner, so old projects 12 | must be exported and imported to a new database. 13 | 14 | * Registering projects and uploading releases now requires 15 | authentication. Use the admin interface to create new users, 16 | registering users via distutils won't be available until 17 | :version:`0.3.0`. 18 | 19 | * Every project now has an owner, so only the user registering the 20 | project can add releases. 21 | 22 | * md5sum is now properly listed in the release link. 23 | 24 | * Project names can now have dots (`.`) in them. 25 | 26 | * Fixed a bug where filenames was mangled if the distribution file 27 | already existed. If someone uploaded version `1.0` of project `grail` 28 | twice, the new filename was renamed to `grail-1.0.tar_.gz`, and a 29 | backup of the old release was kept. Pip couldn't handle these filenames, 30 | so we delete the old release first. 31 | 32 | * Releases now list both project name and version, instead of just version 33 | in the admin interface. 34 | 35 | * Added a sample buildout.cfg. Thanks to Rune Halvorsen (runeh@opera.com). 36 | 37 | 38 | 0.1.0 :date:`2009-03-22 1:21 A.M CET` :author:askh@opera.com 39 | -------------------------------------------------------------- 40 | 41 | * Initial release 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Ask Solem 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | Neither the name of Ask Solem nor the names of its contributors may be used 14 | to endorse or promote products derived from this software without specific 15 | prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 19 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 20 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 21 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include README 3 | include TODO 4 | include MANIFEST.in 5 | include LICENSE 6 | include Changelog 7 | recursive-include djangopypi * 8 | recursive-include chishop * 9 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ========================================= 2 | ChiShop/DjangoPyPI 3 | ========================================= 4 | :Version: 0.1 5 | 6 | Installation 7 | ============ 8 | 9 | Install dependencies:: 10 | 11 | $ python bootstrap.py --distribute 12 | $ ./bin/buildout 13 | 14 | Initial configuration 15 | --------------------- 16 | :: 17 | 18 | $ $EDITOR chishop/settings.py 19 | $ ./bin/django syncdb 20 | 21 | Run the PyPI server 22 | ------------------- 23 | :: 24 | 25 | $ ./bin/django runserver 26 | 27 | Please note that ``chishop/media/dists`` has to be writable by the 28 | user the web-server is running as. 29 | 30 | In production 31 | ------------- 32 | 33 | You may want to copy the file ``chishop/production_example.py`` and modify 34 | for use as your production settings; you will also need to modify 35 | ``bin/django.wsgi`` to refer to your production settings. 36 | 37 | Using Setuptools 38 | ================ 39 | 40 | Add the following to your ``~/.pypirc`` file:: 41 | 42 | [distutils] 43 | index-servers = 44 | pypi 45 | local 46 | 47 | 48 | [pypi] 49 | username:user 50 | password:secret 51 | 52 | [local] 53 | 54 | username:user 55 | password:secret 56 | repository:http://localhost:8000 57 | 58 | Uploading a package: Python >=2.6 59 | -------------------------------------------- 60 | 61 | To push the package to the local pypi:: 62 | 63 | $ python setup.py register -r local sdist upload -r local 64 | 65 | 66 | Uploading a package: Python <2.6 67 | ------------------------------------------- 68 | 69 | If you don't have Python 2.6 please run the command below to install the backport of the extension:: 70 | 71 | $ easy_install -U collective.dist 72 | 73 | instead of using register and dist command, you can use "mregister" and "mupload", that are a backport of python 2.6 register and upload commands, that supports multiple servers. 74 | 75 | To push the package to the local pypi:: 76 | 77 | $ python setup.py mregister -r local sdist mupload -r local 78 | 79 | .. # vim: syntax=rst expandtab tabstop=4 shiftwidth=4 shiftround 80 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | PyPI feature replication 2 | ======================== 3 | 4 | * Make it possible to register users via distutils. 5 | There should be a setting to turn this feature on/off for private PyPIs. 6 | [taken-by: sverrej] 7 | 8 | * Roles (co-owners/maintainers) 9 | - One possible solution: 10 | http://github.com/initcrash/django-object-permissions/tree 11 | I'm not sure what the difference between a co-owner and maintainer is, 12 | maybe it's just a label. 13 | * Package author admin interface (submit, edit, view) 14 | * Documentation upload 15 | * Ratings 16 | * Random Monty Python quotes :-) 17 | * Comments :-) 18 | 19 | Post-PyPI 20 | ========= 21 | 22 | * PEP-381: Mirroring infrastructure for PyPI 23 | [taken-by: jezdez] 24 | 25 | * API to submit test reports for smoke test bots. Like CPAN Testers. 26 | Platform/version/matrix etc. 27 | 28 | * Different listings: Author listings, classifier listings, etc. 29 | 30 | * Search metadata 31 | 32 | * Automatic generation of Sphinx for modules (so you can view them directly 33 | on pypi, like CPAN), Module listing etc. 34 | 35 | * Listing of special files: README, LICENSE, Changefile/Changes, TODO, 36 | MANIFEST. 37 | 38 | * Dependency graphs. 39 | 40 | * Package file browser (like CPAN) 41 | 42 | 43 | 44 | 45 | Documentation 46 | ============= 47 | 48 | * Write a tutorial on how to set up the server, registering projects, and 49 | how to upload releases. 50 | 51 | 52 | -------------------------------------------------------------------------------- /bootstrap.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2006 Zope Corporation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """Bootstrap a buildout-based project 15 | 16 | Simply run this script in a directory containing a buildout.cfg. 17 | The script accepts buildout command-line options, so you can 18 | use the -c option to specify an alternate configuration file. 19 | 20 | $Id$ 21 | """ 22 | 23 | import os, shutil, sys, tempfile, urllib2 24 | from optparse import OptionParser 25 | 26 | tmpeggs = tempfile.mkdtemp() 27 | 28 | is_jython = sys.platform.startswith('java') 29 | 30 | # parsing arguments 31 | parser = OptionParser() 32 | parser.add_option("-v", "--version", dest="version", 33 | help="use a specific zc.buildout version") 34 | parser.add_option("-d", "--distribute", 35 | action="store_true", dest="distribute", default=False, 36 | help="Use Disribute rather than Setuptools.") 37 | 38 | parser.add_option("-c", None, action="store", dest="config_file", 39 | help=("Specify the path to the buildout configuration " 40 | "file to be used.")) 41 | 42 | options, args = parser.parse_args() 43 | 44 | # if -c was provided, we push it back into args for buildout' main function 45 | if options.config_file is not None: 46 | args += ['-c', options.config_file] 47 | 48 | if options.version is not None: 49 | VERSION = '==%s' % options.version 50 | else: 51 | VERSION = '' 52 | 53 | USE_DISTRIBUTE = options.distribute 54 | args = args + ['bootstrap'] 55 | 56 | to_reload = False 57 | try: 58 | import pkg_resources 59 | if not hasattr(pkg_resources, '_distribute'): 60 | to_reload = True 61 | raise ImportError 62 | except ImportError: 63 | ez = {} 64 | if USE_DISTRIBUTE: 65 | exec urllib2.urlopen('http://python-distribute.org/distribute_setup.py' 66 | ).read() in ez 67 | ez['use_setuptools'](to_dir=tmpeggs, download_delay=0, no_fake=True) 68 | else: 69 | exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py' 70 | ).read() in ez 71 | ez['use_setuptools'](to_dir=tmpeggs, download_delay=0) 72 | 73 | if to_reload: 74 | reload(pkg_resources) 75 | else: 76 | import pkg_resources 77 | 78 | if sys.platform == 'win32': 79 | def quote(c): 80 | if ' ' in c: 81 | return '"%s"' % c # work around spawn lamosity on windows 82 | else: 83 | return c 84 | else: 85 | def quote (c): 86 | return c 87 | 88 | cmd = 'from setuptools.command.easy_install import main; main()' 89 | ws = pkg_resources.working_set 90 | 91 | if USE_DISTRIBUTE: 92 | requirement = 'distribute' 93 | else: 94 | requirement = 'setuptools' 95 | 96 | if is_jython: 97 | import subprocess 98 | 99 | assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd', 100 | quote(tmpeggs), 'zc.buildout' + VERSION], 101 | env=dict(os.environ, 102 | PYTHONPATH= 103 | ws.find(pkg_resources.Requirement.parse(requirement)).location 104 | ), 105 | ).wait() == 0 106 | 107 | else: 108 | assert os.spawnle( 109 | os.P_WAIT, sys.executable, quote (sys.executable), 110 | '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout' + VERSION, 111 | dict(os.environ, 112 | PYTHONPATH= 113 | ws.find(pkg_resources.Requirement.parse(requirement)).location 114 | ), 115 | ) == 0 116 | 117 | ws.add_entry(tmpeggs) 118 | ws.require('zc.buildout' + VERSION) 119 | import zc.buildout.buildout 120 | zc.buildout.buildout.main(args) 121 | shutil.rmtree(tmpeggs) 122 | -------------------------------------------------------------------------------- /buildout.cfg: -------------------------------------------------------------------------------- 1 | [buildout] 2 | parts = django 3 | find-links = http://bitbucket.org/ubernostrum/django-registration/downloads/django-registration-0.8-alpha-1.tar.gz 4 | unzip = true 5 | eggs = pkginfo 6 | django-registration==0.8-alpha-1 7 | 8 | [django] 9 | recipe = djangorecipe 10 | version = 1.1.1 11 | settings = settings 12 | eggs = ${buildout:eggs} 13 | test = djangopypi 14 | project = chishop 15 | wsgi = true 16 | -------------------------------------------------------------------------------- /chishop/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 1, 1) 2 | __version__ = ".".join(map(str, VERSION)) 3 | -------------------------------------------------------------------------------- /chishop/conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ask/chishop/97f397369d726c3d50f4f3c4e8fad88e45f7295c/chishop/conf/__init__.py -------------------------------------------------------------------------------- /chishop/conf/default.py: -------------------------------------------------------------------------------- 1 | # Django settings for djangopypi project. 2 | import os 3 | 4 | ADMINS = ( 5 | # ('Your Name', 'your_email@domain.com'), 6 | ) 7 | 8 | # Allow uploading a new distribution file for a project version 9 | # if a file of that type already exists. 10 | # 11 | # The default on PyPI is to not allow this, but it can be real handy 12 | # if you're sloppy. 13 | DJANGOPYPI_ALLOW_VERSION_OVERWRITE = False 14 | DJANGOPYPI_RELEASE_UPLOAD_TO = 'dists' 15 | 16 | # change to False if you do not want Django's default server to serve static pages 17 | LOCAL_DEVELOPMENT = True 18 | 19 | REGISTRATION_OPEN = True 20 | ACCOUNT_ACTIVATION_DAYS = 7 21 | LOGIN_REDIRECT_URL = "/" 22 | 23 | EMAIL_HOST = 'localhost' 24 | DEFAULT_FROM_EMAIL = '' 25 | SERVER_EMAIL = DEFAULT_FROM_EMAIL 26 | 27 | MANAGERS = ADMINS 28 | 29 | DATABASE_ENGINE = '' 30 | DATABASE_NAME = '' 31 | DATABASE_USER = '' 32 | DATABASE_PASSWORD = '' 33 | DATABASE_HOST = '' 34 | DATABASE_PORT = '' 35 | 36 | # Local time zone for this installation. Choices can be found here: 37 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 38 | # although not all choices may be available on all operating systems. 39 | # If running in a Windows environment this must be set to the same as your 40 | # system time zone. 41 | TIME_ZONE = 'America/Chicago' 42 | 43 | # Language code for this installation. All choices can be found here: 44 | # http://www.i18nguy.com/unicode/language-identifiers.html 45 | LANGUAGE_CODE = 'en-us' 46 | 47 | SITE_ID = 1 48 | 49 | # If you set this to False, Django will make some optimizations so as not 50 | # to load the internationalization machinery. 51 | USE_I18N = True 52 | 53 | # Absolute path to the directory that holds media. 54 | # Example: "/home/media/media.lawrence.com/" 55 | here = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) 56 | MEDIA_ROOT = os.path.join(here, 'media') 57 | 58 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 59 | # trailing slash if there is a path component (optional in other cases). 60 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 61 | MEDIA_URL = '/media/' 62 | 63 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 64 | # trailing slash. 65 | # Examples: "http://foo.com/media/", "/media/". 66 | ADMIN_MEDIA_PREFIX = '/admin/media/' 67 | 68 | # Make this unique, and don't share it with anybody. 69 | SECRET_KEY = 'w_#0r2hh)=!zbynb*gg&969@)sy#^-^ia3m*+sd4@lst$zyaxu' 70 | 71 | # List of callables that know how to import templates from various sources. 72 | TEMPLATE_LOADERS = ( 73 | 'django.template.loaders.filesystem.load_template_source', 74 | 'django.template.loaders.app_directories.load_template_source', 75 | # 'django.template.loaders.eggs.load_template_source', 76 | ) 77 | 78 | MIDDLEWARE_CLASSES = ( 79 | 'django.middleware.common.CommonMiddleware', 80 | 'django.contrib.sessions.middleware.SessionMiddleware', 81 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 82 | ) 83 | 84 | ROOT_URLCONF = 'chishop.urls' 85 | 86 | TEMPLATE_CONTEXT_PROCESSORS = ( 87 | "django.core.context_processors.auth", 88 | "django.core.context_processors.debug", 89 | "django.core.context_processors.i18n", 90 | "django.core.context_processors.media", 91 | "django.core.context_processors.request", 92 | ) 93 | 94 | TEMPLATE_DIRS = ( 95 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 96 | # Always use forward slashes, even on Windows. 97 | # Don't forget to use absolute paths, not relative paths. 98 | os.path.join(os.path.dirname(os.path.dirname(__file__)), "templates"), 99 | ) 100 | 101 | INSTALLED_APPS = ( 102 | 'django.contrib.auth', 103 | 'django.contrib.contenttypes', 104 | 'django.contrib.sessions', 105 | 'django.contrib.sites', 106 | 'django.contrib.admin', 107 | 'django.contrib.markup', 108 | 'django.contrib.admindocs', 109 | 'registration', 110 | 'djangopypi', 111 | ) 112 | -------------------------------------------------------------------------------- /chishop/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | 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__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /chishop/media/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ask/chishop/97f397369d726c3d50f4f3c4e8fad88e45f7295c/chishop/media/__init__.py -------------------------------------------------------------------------------- /chishop/media/dists/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ask/chishop/97f397369d726c3d50f4f3c4e8fad88e45f7295c/chishop/media/dists/__init__.py -------------------------------------------------------------------------------- /chishop/media/style/djangopypi.css: -------------------------------------------------------------------------------- 1 | .search { 2 | text-align:right; 3 | margin-right: 10px; 4 | } -------------------------------------------------------------------------------- /chishop/production_example.py: -------------------------------------------------------------------------------- 1 | from conf.default import * 2 | import os 3 | 4 | DEBUG = False 5 | TEMPLATE_DEBUG = DEBUG 6 | 7 | ADMINS = ( 8 | ('chishop', 'example@example.org'), 9 | ) 10 | 11 | MANAGERS = ADMINS 12 | 13 | DATABASE_ENGINE = 'postgresql_psycopg2' 14 | DATABASE_NAME = 'chishop' 15 | DATABASE_USER = 'chishop' 16 | DATABASE_PASSWORD = 'chishop' 17 | DATABASE_HOST = '' 18 | DATABASE_PORT = '' 19 | -------------------------------------------------------------------------------- /chishop/settings.py: -------------------------------------------------------------------------------- 1 | from conf.default import * 2 | import os 3 | 4 | DEBUG = True 5 | TEMPLATE_DEBUG = DEBUG 6 | LOCAL_DEVELOPMENT = True 7 | 8 | if LOCAL_DEVELOPMENT: 9 | import sys 10 | sys.path.append(os.path.dirname(__file__)) 11 | 12 | ADMINS = ( 13 | ('chishop', 'example@example.org'), 14 | ) 15 | 16 | MANAGERS = ADMINS 17 | 18 | DATABASE_ENGINE = 'sqlite3' 19 | DATABASE_NAME = os.path.join(here, 'devdatabase.db') 20 | DATABASE_USER = '' 21 | DATABASE_PASSWORD = '' 22 | DATABASE_HOST = '' 23 | DATABASE_PORT = '' 24 | -------------------------------------------------------------------------------- /chishop/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans 'Page not found' %}{% endblock %} 5 | 6 | {% block content %} 7 | 8 |

{% trans 'Page not found' %}

9 | 10 |

{% trans "We're sorry, but the requested page could not be found." %}

11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /chishop/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n %} 3 | 4 | {% block breadcrumbs %}{% endblock %} 5 | 6 | {% block title %}{% trans 'Server error (500)' %}{% endblock %} 7 | 8 | {% block content %} 9 |

{% trans 'Server Error (500)' %}

10 |

{% trans "There's been an error. It's been reported to the site administrators via e-mail and should be fixed shortly. Thanks for your patience." %}

11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /chishop/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{{ title }} | {% trans 'Chishop site admin' %}{% endblock %} 5 | 6 | {% block branding %} 7 |

{% trans 'Chishop' %}

8 | {% endblock %} 9 | 10 | {% block nav-global %}{% endblock %} 11 | -------------------------------------------------------------------------------- /chishop/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block extrastyle %}{% endblock %} 7 | 8 | {% block title %}{% endblock %} 9 | 10 | {% block site_extrahead %}{% endblock %} 11 | {% block extrahead %}{% endblock %} 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 39 | 40 | 41 | 44 | {% block messagelist %} 45 | {% if messages %} 46 |
    47 | {% for message in messages %} 48 |
  • {{ message }}
  • 49 | {% endfor %} 50 |
51 | {% endif %} 52 | {% endblock %} 53 | 54 | 55 |
56 | {% block pretitle %}{% endblock %} 57 |

{% block content_title %}{% endblock %}

58 |
59 | {% block objecttools %}{% endblock %} 60 | {% block sidebar %}{% endblock %} 61 | {% block content %}{{ content }}{% endblock %} 62 |
63 |
64 |
65 | 66 | 67 | 68 | 72 | 73 |
74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /chishop/templates/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ title }} | Chishop{% endblock %} 4 | 5 | {% block bread_crumbs_1 %} 6 | › {{ title }} 7 | {% endblock %} 8 | {% block site_name_header %} 9 | Chishop 10 | {% endblock %} 11 | 12 | {% block content_title %}{{ title }}{% endblock %} 13 | 14 | {% block nav-global %}{% endblock %} 15 | -------------------------------------------------------------------------------- /chishop/templates/djangopypi/pypi.html: -------------------------------------------------------------------------------- 1 | {% extends "base_site.html" %} 2 | 3 | {% block bread_crumbs_1 %}{% endblock %} 4 | 5 | {% block content %} 6 | 7 | 8 | {% for dist in dists %} 9 | 10 | 12 | 13 | 14 | {% endfor %} 15 |
UpdatedPackageSummary
{{ dist.updated|date:"d/m/y" }} 11 | {{ dist.name }}{{ dist.summary|truncatewords:10 }}
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /chishop/templates/djangopypi/pypi_show_links.html: -------------------------------------------------------------------------------- 1 | {% extends "base_site.html" %} 2 | 3 | {% block content %} 4 |

5 | {{ project.summary }} 6 |

7 | {% load safemarkup %} 8 | {{ project.description|saferst }} 9 | 10 |
11 | 12 | 13 | {% for release in releases %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% endfor %} 23 |
FilenamePlatformTypeVersionUploaded OnSize
{{ release.filename }}{{ release.platform }}{{ release.type }}{{ release.version }}{{ release.upload_time }}{{ release.distribution.size|filesizeformat }}
24 |
25 | 38 | {% endblock %} 39 | 40 | -------------------------------------------------------------------------------- /chishop/templates/djangopypi/search.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /chishop/templates/djangopypi/search_results.html: -------------------------------------------------------------------------------- 1 | {% extends "base_site.html" %} 2 | 3 | {% block bread_crumbs_1 %}›Search{% endblock %} 4 | 5 | {% block content %} 6 | {% ifnotequal search_term ''%} 7 |

Index of Packages Matching '{{ search_term }}'

8 | {% else %} 9 |

You need to supply a search term.

10 | {% endifnotequal %} 11 | {% if dists %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% for dist in dists %} 20 | 21 | 23 | 24 | 25 | {% endfor %} 26 | 27 |
UpdatedPackageSummary
{{ dist.updated|date:"d/m/y" }} 22 | {{ dist.name }}{{ dist.summary|truncatewords:10 }}
28 | {% else %} 29 | There were no matches. 30 | {% endif %} 31 | {% endblock content %} -------------------------------------------------------------------------------- /chishop/templates/djangopypi/show_links.html: -------------------------------------------------------------------------------- 1 | Links for {{ dist_name }} 2 |

Links for {{ dist_name }}

3 | 4 | {% for release in releases %} 5 | {{ release.filename }}
6 | {% endfor %} 7 | 8 | -------------------------------------------------------------------------------- /chishop/templates/djangopypi/show_version.html: -------------------------------------------------------------------------------- 1 | Links for {{ dist_name }} {{ version }} 2 |

Links for {{ dist_name }} {{ version }}

3 | 4 | {{ release.filename }}
5 | 6 | -------------------------------------------------------------------------------- /chishop/templates/djangopypi/simple.html: -------------------------------------------------------------------------------- 1 | Simple Index 2 | {% for dist in dists %} 3 | {{ dist.name }}
4 | {% endfor %} 5 | 6 | -------------------------------------------------------------------------------- /chishop/templates/registration/activate.html: -------------------------------------------------------------------------------- 1 | {% extends "base_site.html" %} 2 | 3 | {% block content %} 4 |

Activation Failed

5 |

6 | Activation with key {{activation_key}} failed. 7 |

8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /chishop/templates/registration/activation_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "base_site.html" %} 2 | 3 | {% block content %} 4 |

Activation complete.

5 |

6 | Hello {{user}}, you are registered. 7 | Go here to get back to the main page. 8 |

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /chishop/templates/registration/activation_email.txt: -------------------------------------------------------------------------------- 1 | Welcome to Chishop. 2 | 3 | Please click here to activate your account: 4 | http://{{site}}/accounts/activate/{{activation_key}}/ 5 | 6 | Account has to be activated within {{expiration_days}} days. 7 | -------------------------------------------------------------------------------- /chishop/templates/registration/activation_email_subject.txt: -------------------------------------------------------------------------------- 1 | Account Activation - {{ site }} 2 | -------------------------------------------------------------------------------- /chishop/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base_site.html" %} 2 | 3 | {% block content %} 4 |
5 | {{form.as_p}} 6 | 7 |
8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /chishop/templates/registration/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "base_site.html" %} 2 | 3 | {% block main_content %} 4 |

5 | {%trans "Logged out."%} 6 |

7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /chishop/templates/registration/registration_closed.html: -------------------------------------------------------------------------------- 1 | {% extends "base_site.html" %} 2 | 3 | {% block content %} 4 |

Registration Closed

5 |

6 | Registration is disabled. 7 |

8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /chishop/templates/registration/registration_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "base_site.html" %} 2 | 3 | {% block content %} 4 |

Registration complete

5 |

6 | An activation mail has been sent to you. 7 |

8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /chishop/templates/registration/registration_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base_site.html" %} 2 | 3 | {% block content %} 4 |

Register

5 |
6 | {{form.as_p}} 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /chishop/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.conf.urls.defaults import patterns, url, include, handler404, handler500 3 | from django.conf import settings 4 | from django.contrib import admin 5 | 6 | admin.autodiscover() 7 | 8 | urlpatterns = patterns('') 9 | 10 | # Serve static pages. 11 | if settings.LOCAL_DEVELOPMENT: 12 | urlpatterns += patterns("django.views", 13 | url(r"^%s(?P.*)$" % settings.MEDIA_URL[1:], "static.serve", { 14 | "document_root": settings.MEDIA_ROOT})) 15 | 16 | urlpatterns += patterns("", 17 | # Admin interface 18 | url(r'^admin/doc/', include("django.contrib.admindocs.urls")), 19 | url(r'^admin/(.*)', admin.site.root), 20 | 21 | # Registration 22 | url(r'^accounts/', include('registration.backends.default.urls')), 23 | 24 | # The Chishop 25 | url(r'', include("djangopypi.urls")) 26 | ) 27 | -------------------------------------------------------------------------------- /djangopypi/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 2, 0) 2 | __version__ = ".".join(map(str, VERSION)) 3 | -------------------------------------------------------------------------------- /djangopypi/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from djangopypi.models import Project, Release, Classifier 3 | 4 | admin.site.register(Project) 5 | admin.site.register(Release) 6 | admin.site.register(Classifier) 7 | -------------------------------------------------------------------------------- /djangopypi/forms.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django import forms 3 | from django.conf import settings 4 | from djangopypi.models import Project, Classifier, Release 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | 8 | class ProjectForm(forms.ModelForm): 9 | class Meta: 10 | model = Project 11 | exclude = ['owner', 'classifiers'] 12 | 13 | 14 | class ReleaseForm(forms.ModelForm): 15 | class Meta: 16 | model = Release 17 | exclude = ['project'] -------------------------------------------------------------------------------- /djangopypi/http.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.core.files.uploadedfile import SimpleUploadedFile 3 | from django.utils.datastructures import MultiValueDict 4 | from django.contrib.auth import authenticate 5 | 6 | 7 | class HttpResponseNotImplemented(HttpResponse): 8 | status_code = 501 9 | 10 | 11 | class HttpResponseUnauthorized(HttpResponse): 12 | status_code = 401 13 | 14 | def __init__(self, realm): 15 | HttpResponse.__init__(self) 16 | self['WWW-Authenticate'] = 'Basic realm="%s"' % realm 17 | 18 | 19 | def parse_distutils_request(request): 20 | raw_post_data = request.raw_post_data 21 | sep = raw_post_data.splitlines()[1] 22 | items = raw_post_data.split(sep) 23 | post_data = {} 24 | files = {} 25 | for part in filter(lambda e: not e.isspace(), items): 26 | item = part.splitlines() 27 | if len(item) < 2: 28 | continue 29 | header = item[1].replace("Content-Disposition: form-data; ", "") 30 | kvpairs = header.split(";") 31 | headers = {} 32 | for kvpair in kvpairs: 33 | if not kvpair: 34 | continue 35 | key, value = kvpair.split("=") 36 | headers[key] = value.strip('"') 37 | if "name" not in headers: 38 | continue 39 | content = part[len("\n".join(item[0:2]))+2:len(part)-1] 40 | if "filename" in headers: 41 | file = SimpleUploadedFile(headers["filename"], content, 42 | content_type="application/gzip") 43 | files["distribution"] = [file] 44 | elif headers["name"] in post_data: 45 | post_data[headers["name"]].append(content) 46 | else: 47 | # Distutils sends UNKNOWN for empty fields (e.g platform) 48 | # [russell.sim@gmail.com] 49 | if content == 'UNKNOWN': 50 | post_data[headers["name"]] = [None] 51 | else: 52 | post_data[headers["name"]] = [content] 53 | 54 | return MultiValueDict(post_data), MultiValueDict(files) 55 | 56 | 57 | def login_basic_auth(request): 58 | authentication = request.META.get("HTTP_AUTHORIZATION") 59 | if not authentication: 60 | return 61 | (authmeth, auth) = authentication.split(' ', 1) 62 | if authmeth.lower() != "basic": 63 | return 64 | auth = auth.strip().decode("base64") 65 | username, password = auth.split(":", 1) 66 | return authenticate(username=username, password=password) 67 | -------------------------------------------------------------------------------- /djangopypi/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ask/chishop/97f397369d726c3d50f4f3c4e8fad88e45f7295c/djangopypi/management/__init__.py -------------------------------------------------------------------------------- /djangopypi/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ask/chishop/97f397369d726c3d50f4f3c4e8fad88e45f7295c/djangopypi/management/commands/__init__.py -------------------------------------------------------------------------------- /djangopypi/management/commands/loadclassifiers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Management command for loading all the known classifiers from the official 3 | pypi, or from a file/url. 4 | 5 | Note, pypi docs says to not add classifiers that are not used in submitted 6 | projects. On the other hand it can be usefull to have a list of classifiers 7 | to choose if you have to modify package data. Use it if you need it. 8 | """ 9 | 10 | from __future__ import with_statement 11 | import urllib 12 | import os.path 13 | 14 | from django.core.management.base import BaseCommand 15 | from djangopypi.models import Classifier 16 | 17 | CLASSIFIERS_URL = "http://pypi.python.org/pypi?%3Aaction=list_classifiers" 18 | 19 | class Command(BaseCommand): 20 | help = """Load all classifiers from pypi. If any arguments are given, 21 | they will be used as paths or urls for classifiers instead of using the 22 | official pypi list url""" 23 | 24 | def handle(self, *args, **options): 25 | args = args or [CLASSIFIERS_URL] 26 | 27 | cnt = 0 28 | for location in args: 29 | print "Loading %s" % location 30 | lines = self._get_lines(location) 31 | for name in lines: 32 | c, created = Classifier.objects.get_or_create(name=name) 33 | if created: 34 | c.save() 35 | cnt += 1 36 | 37 | print "Added %s new classifiers from %s source(s)" % (cnt, len(args)) 38 | 39 | def _get_lines(self, location): 40 | """Return a list of lines for a lication that can be a file or 41 | a url. If path/url doesn't exist, returns an empty list""" 42 | try: # This is dirty, but OK I think. both net and file ops raise IOE 43 | if location.startswith(("http://", "https://")): 44 | fp = urllib.urlopen(location) 45 | return [e.strip() for e in fp.read().split('\n') 46 | if e and not e.isspace()] 47 | else: 48 | fp = open(location) 49 | return [e.strip() for e in fp.readlines() 50 | if e and not e.isspace()] 51 | except IOError: 52 | print "Couldn't load %s" % location 53 | return [] 54 | -------------------------------------------------------------------------------- /djangopypi/management/commands/ppadd.py: -------------------------------------------------------------------------------- 1 | """ 2 | Management command for adding a package to the repository. Supposed to be the 3 | equivelant of calling easy_install, but the install target is the chishop. 4 | """ 5 | 6 | from __future__ import with_statement 7 | import os 8 | import tempfile 9 | import shutil 10 | import urllib 11 | 12 | import pkginfo 13 | 14 | from django.core.files.base import File 15 | from django.core.management.base import LabelCommand 16 | from optparse import make_option 17 | from contextlib import contextmanager 18 | from urlparse import urlsplit 19 | from setuptools.package_index import PackageIndex 20 | from django.contrib.auth.models import User 21 | from djangopypi.models import Project, Release, Classifier 22 | 23 | 24 | 25 | 26 | 27 | @contextmanager 28 | def tempdir(): 29 | """Simple context that provides a temporary directory that is deleted 30 | when the context is exited.""" 31 | d = tempfile.mkdtemp(".tmp", "djangopypi.") 32 | yield d 33 | shutil.rmtree(d) 34 | 35 | class Command(LabelCommand): 36 | option_list = LabelCommand.option_list + ( 37 | make_option("-o", "--owner", help="add packages as OWNER", 38 | metavar="OWNER", default=None), 39 | ) 40 | help = """Add one or more packages to the repository. Each argument can 41 | be a package name or a URL to an archive or egg. Package names honour 42 | the same rules as easy_install with regard to indicating versions etc. 43 | 44 | If a version of the package exists, but is older than what we want to install, 45 | the owner remains the same. 46 | 47 | For new packages there needs to be an owner. If the --owner option is present 48 | we use that value. If not, we try to match the maintainer of the package, form 49 | the metadata, with a user in out database, based on the If it's a new package 50 | and the maintainer emailmatches someone in our user list, we use that. If not, 51 | the package can not be 52 | added""" 53 | 54 | def __init__(self, *args, **kwargs): 55 | self.pypi = PackageIndex() 56 | LabelCommand.__init__(self, *args, **kwargs) 57 | 58 | def handle_label(self, label, **options): 59 | with tempdir() as tmp: 60 | path = self.pypi.download(label, tmp) 61 | if path: 62 | self._save_package(path, options["owner"]) 63 | else: 64 | print "Could not add %s. Not found." % label 65 | 66 | def _save_package(self, path, ownerid): 67 | meta = self._get_meta(path) 68 | 69 | try: 70 | # can't use get_or_create as that demands there be an owner 71 | project = Project.objects.get(name=meta.name) 72 | isnewproject = False 73 | except Project.DoesNotExist: 74 | project = Project(name=meta.name) 75 | isnewproject = True 76 | 77 | release = project.get_release(meta.version) 78 | if not isnewproject and release and release.version == meta.version: 79 | print "%s-%s already added" % (meta.name, meta.version) 80 | return 81 | 82 | # algorithm as follows: If owner is given, try to grab user with that 83 | # username from db. If doesn't exist, bail. If no owner set look at 84 | # mail address from metadata and try to get that user. If it exists 85 | # use it. If not, bail. 86 | owner = None 87 | 88 | if ownerid: 89 | try: 90 | if "@" in ownerid: 91 | owner = User.objects.get(email=ownerid) 92 | else: 93 | owner = User.objects.get(username=ownerid) 94 | except User.DoesNotExist: 95 | pass 96 | else: 97 | try: 98 | owner = User.objects.get(email=meta.author_email) 99 | except User.DoesNotExist: 100 | pass 101 | 102 | if not owner: 103 | print "No owner defined. Use --owner to force one" 104 | return 105 | 106 | # at this point we have metadata and an owner, can safely add it. 107 | 108 | project.owner = owner 109 | # Some packages don't have proper licence, seems to be a problem 110 | # with setup.py upload. Use "UNKNOWN" 111 | project.license = meta.license or "Unknown" 112 | project.metadata_version = meta.metadata_version 113 | project.author = meta.author 114 | project.home_page = meta.home_page 115 | project.download_url = meta.download_url 116 | project.summary = meta.summary 117 | project.description = meta.description 118 | project.author_email = meta.author_email 119 | 120 | project.save() 121 | 122 | for classifier in meta.classifiers: 123 | project.classifiers.add( 124 | Classifier.objects.get_or_create(name=classifier)[0]) 125 | 126 | release = Release() 127 | release.version = meta.version 128 | release.project = project 129 | filename = os.path.basename(path) 130 | 131 | file = File(open(path, "rb")) 132 | release.distribution.save(filename, file) 133 | release.save() 134 | print "%s-%s added" % (meta.name, meta.version) 135 | 136 | def _get_meta(self, path): 137 | data = pkginfo.get_metadata(path) 138 | if data: 139 | return data 140 | else: 141 | print "Couldn't get metadata from %s. Not added to chishop" % os.path.basename(path) 142 | return None 143 | -------------------------------------------------------------------------------- /djangopypi/models.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.conf import settings 3 | from django.db import models 4 | from django.utils.translation import ugettext_lazy as _ 5 | from django.contrib.auth.models import User 6 | 7 | OS_NAMES = ( 8 | ("aix", "AIX"), 9 | ("beos", "BeOS"), 10 | ("debian", "Debian Linux"), 11 | ("dos", "DOS"), 12 | ("freebsd", "FreeBSD"), 13 | ("hpux", "HP/UX"), 14 | ("mac", "Mac System x."), 15 | ("macos", "MacOS X"), 16 | ("mandrake", "Mandrake Linux"), 17 | ("netbsd", "NetBSD"), 18 | ("openbsd", "OpenBSD"), 19 | ("qnx", "QNX"), 20 | ("redhat", "RedHat Linux"), 21 | ("solaris", "SUN Solaris"), 22 | ("suse", "SuSE Linux"), 23 | ("yellowdog", "Yellow Dog Linux"), 24 | ) 25 | 26 | ARCHITECTURES = ( 27 | ("alpha", "Alpha"), 28 | ("hppa", "HPPA"), 29 | ("ix86", "Intel"), 30 | ("powerpc", "PowerPC"), 31 | ("sparc", "Sparc"), 32 | ("ultrasparc", "UltraSparc"), 33 | ) 34 | 35 | UPLOAD_TO = getattr(settings, 36 | "DJANGOPYPI_RELEASE_UPLOAD_TO", 'dist') 37 | 38 | class Classifier(models.Model): 39 | name = models.CharField(max_length=255, unique=True) 40 | 41 | class Meta: 42 | verbose_name = _(u"classifier") 43 | verbose_name_plural = _(u"classifiers") 44 | 45 | def __unicode__(self): 46 | return self.name 47 | 48 | 49 | class Project(models.Model): 50 | name = models.CharField(max_length=255, unique=True) 51 | license = models.TextField(blank=True) 52 | metadata_version = models.CharField(max_length=64, default=1.0) 53 | author = models.CharField(max_length=128, blank=True) 54 | home_page = models.URLField(verify_exists=False, blank=True, null=True) 55 | download_url = models.CharField(max_length=200, blank=True, null=True) 56 | summary = models.TextField(blank=True) 57 | description = models.TextField(blank=True) 58 | author_email = models.CharField(max_length=255, blank=True) 59 | classifiers = models.ManyToManyField(Classifier) 60 | owner = models.ForeignKey(User, related_name="projects") 61 | updated = models.DateTimeField(auto_now=True) 62 | 63 | class Meta: 64 | verbose_name = _(u"project") 65 | verbose_name_plural = _(u"projects") 66 | 67 | def __unicode__(self): 68 | return self.name 69 | 70 | @models.permalink 71 | def get_absolute_url(self): 72 | return ('djangopypi-show_links', (), {'dist_name': self.name}) 73 | 74 | @models.permalink 75 | def get_pypi_absolute_url(self): 76 | return ('djangopypi-pypi_show_links', (), {'dist_name': self.name}) 77 | 78 | def get_release(self, version): 79 | """Return the release object for version, or None""" 80 | try: 81 | return self.releases.get(version=version) 82 | except Release.DoesNotExist: 83 | return None 84 | 85 | class Release(models.Model): 86 | version = models.CharField(max_length=32) 87 | distribution = models.FileField(upload_to=UPLOAD_TO) 88 | md5_digest = models.CharField(max_length=255, blank=True) 89 | platform = models.CharField(max_length=128, blank=True) 90 | signature = models.CharField(max_length=128, blank=True) 91 | filetype = models.CharField(max_length=255, blank=True) 92 | pyversion = models.CharField(max_length=32, blank=True) 93 | project = models.ForeignKey(Project, related_name="releases") 94 | upload_time = models.DateTimeField(auto_now=True) 95 | 96 | class Meta: 97 | verbose_name = _(u"release") 98 | verbose_name_plural = _(u"releases") 99 | unique_together = ("project", "version", "platform", "distribution", "pyversion") 100 | 101 | def __unicode__(self): 102 | return u"%s (%s)" % (self.release_name, self.platform) 103 | 104 | @property 105 | def type(self): 106 | dist_file_types = { 107 | 'sdist':'Source', 108 | 'bdist_dumb':'"dumb" binary', 109 | 'bdist_rpm':'RPM', 110 | 'bdist_wininst':'MS Windows installer', 111 | 'bdist_egg':'Python Egg', 112 | 'bdist_dmg':'OS X Disk Image'} 113 | return dist_file_types.get(self.filetype, self.filetype) 114 | 115 | @property 116 | def filename(self): 117 | return os.path.basename(self.distribution.name) 118 | 119 | @property 120 | def release_name(self): 121 | return u"%s-%s" % (self.project.name, self.version) 122 | 123 | @property 124 | def path(self): 125 | return self.distribution.name 126 | 127 | @models.permalink 128 | def get_absolute_url(self): 129 | return ('djangopypi-show_version', (), {'dist_name': self.project, 'version': self.version}) 130 | 131 | def get_dl_url(self): 132 | return "%s#md5=%s" % (self.distribution.url, self.md5_digest) 133 | -------------------------------------------------------------------------------- /djangopypi/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ask/chishop/97f397369d726c3d50f4f3c4e8fad88e45f7295c/djangopypi/templatetags/__init__.py -------------------------------------------------------------------------------- /djangopypi/templatetags/safemarkup.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | from django.utils.encoding import smart_str, force_unicode 4 | from django.utils.safestring import mark_safe 5 | 6 | register = template.Library() 7 | 8 | 9 | def saferst(value): 10 | try: 11 | from docutils.core import publish_parts 12 | except ImportError: 13 | return force_unicode(value) 14 | 15 | docutils_settings = getattr(settings, "RESTRUCTUREDTEXT_FILTER_SETTINGS", 16 | dict()) 17 | 18 | try: 19 | parts = publish_parts(source=smart_str(value), 20 | writer_name="html4css1", 21 | settings_overrides=docutils_settings) 22 | except: 23 | return force_unicode(value) 24 | else: 25 | return mark_safe(force_unicode(parts["fragment"])) 26 | saferst.is_safe = True 27 | register.filter(saferst) 28 | 29 | -------------------------------------------------------------------------------- /djangopypi/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import StringIO 3 | from djangopypi.views import parse_distutils_request, simple 4 | from djangopypi.models import Project, Classifier 5 | from django.test.client import Client 6 | from django.core.urlresolvers import reverse 7 | from django.contrib.auth.models import User 8 | from django.http import HttpRequest 9 | 10 | def create_post_data(action): 11 | data = { 12 | ":action": action, 13 | "metadata_version": "1.0", 14 | "name": "foo", 15 | "version": "0.1.0-pre2", 16 | "summary": "The quick brown fox jumps over the lazy dog.", 17 | "home_page": "http://example.com", 18 | "author": "Foo Bar Baz", 19 | "author_email": "foobarbaz@example.com", 20 | "license": "Apache", 21 | "keywords": "foo bar baz", 22 | "platform": "UNKNOWN", 23 | "classifiers": [ 24 | "Development Status :: 3 - Alpha", 25 | "Environment :: Web Environment", 26 | "Framework :: Django", 27 | "Operating System :: OS Independent", 28 | "Intended Audience :: Developers", 29 | "Intended Audience :: System Administrators", 30 | "License :: OSI Approved :: BSD License", 31 | "Topic :: System :: Software Distribution", 32 | "Programming Language :: Python", 33 | ], 34 | "download_url": "", 35 | "provides": "", 36 | "requires": "", 37 | "obsoletes": "", 38 | "description": """ 39 | ========= 40 | FOOBARBAZ 41 | ========= 42 | 43 | Introduction 44 | ------------ 45 | ``foo`` :class:`bar` 46 | *baz* 47 | [foaoa] 48 | """, 49 | } 50 | return data 51 | 52 | def create_request(data): 53 | boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' 54 | sep_boundary = '\n--' + boundary 55 | end_boundary = sep_boundary + '--' 56 | body = StringIO.StringIO() 57 | for key, value in data.items(): 58 | # handle multiple entries for the same name 59 | if type(value) not in (type([]), type( () )): 60 | value = [value] 61 | for value in value: 62 | value = unicode(value).encode("utf-8") 63 | body.write(sep_boundary) 64 | body.write('\nContent-Disposition: form-data; name="%s"'%key) 65 | body.write("\n\n") 66 | body.write(value) 67 | if value and value[-1] == '\r': 68 | body.write('\n') # write an extra newline (lurve Macs) 69 | body.write(end_boundary) 70 | body.write("\n") 71 | 72 | return body.getvalue() 73 | 74 | 75 | class MockRequest(object): 76 | 77 | def __init__(self, raw_post_data): 78 | self.raw_post_data = raw_post_data 79 | self.META = {} 80 | 81 | 82 | class TestParseWeirdPostData(unittest.TestCase): 83 | 84 | def test_weird_post_data(self): 85 | data = create_post_data("submit") 86 | raw_post_data = create_request(data) 87 | post, files = parse_distutils_request(MockRequest(raw_post_data)) 88 | self.assertTrue(post) 89 | 90 | for key in post.keys(): 91 | if isinstance(data[key], list): 92 | self.assertEquals(data[key], post.getlist(key)) 93 | elif data[key] == "UNKNOWN": 94 | self.assertTrue(post[key] is None) 95 | else: 96 | self.assertEquals(post[key], data[key]) 97 | 98 | 99 | 100 | client = Client() 101 | 102 | class TestSearch(unittest.TestCase): 103 | 104 | def setUp(self): 105 | dummy_user = User.objects.create(username='krill', password='12345', 106 | email='krill@opera.com') 107 | Project.objects.create(name='foo', license='Gnu', 108 | summary="The quick brown fox jumps over the lazy dog.", 109 | owner=dummy_user) 110 | 111 | def test_search_for_package(self): 112 | response = client.post(reverse('djangopypi-search'), {'search_term': 'foo'}) 113 | self.assertTrue("The quick brown fox jumps over the lazy dog." in response.content) 114 | 115 | class TestSimpleView(unittest.TestCase): 116 | 117 | def create_distutils_httprequest(self, user_data={}): 118 | self.post_data = create_post_data(action='user') 119 | self.post_data.update(user_data) 120 | self.raw_post_data = create_request(self.post_data) 121 | request = HttpRequest() 122 | request.POST = self.post_data 123 | request.method = "POST" 124 | request.raw_post_data = self.raw_post_data 125 | return request 126 | 127 | def test_user_registration(self): 128 | request = self.create_distutils_httprequest({'name': 'peter_parker', 'email':'parker@dailybugle.com', 129 | 'password':'spiderman'}) 130 | response = simple(request) 131 | self.assertEquals(200, response.status_code) 132 | 133 | def test_user_registration_with_wrong_data(self): 134 | request = self.create_distutils_httprequest({'name': 'peter_parker', 'email':'parker@dailybugle.com', 135 | 'password':'',}) 136 | response = simple(request) 137 | self.assertEquals(400, response.status_code) 138 | 139 | -------------------------------------------------------------------------------- /djangopypi/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.conf.urls.defaults import patterns, url, include 3 | 4 | urlpatterns = patterns("djangopypi.views", 5 | # Simple PyPI 6 | url(r'^simple/$', "simple", 7 | name="djangopypi-simple"), 8 | 9 | url(r'^simple/(?P[\w\d_\.\-]+)/(?P[\w\.\d\-_]+)/$', 10 | "show_version", 11 | name="djangopypi-show_version"), 12 | 13 | url(r'^simple/(?P[\w\d_\.\-]+)/$', "show_links", 14 | name="djangopypi-show_links"), 15 | 16 | url(r'^$', "simple", {'template_name': 'djangopypi/pypi.html'}, 17 | name="djangopypi-pypi"), 18 | 19 | url(r'^(?P[\w\d_\.\-]+)/$', "show_links", 20 | {'template_name': 'djangopypi/pypi_show_links.html'}, 21 | name="djangopypi-pypi_show_links"), 22 | 23 | url(r'^search','search',name='djangopypi-search') 24 | ) -------------------------------------------------------------------------------- /djangopypi/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | 4 | from django.core.files.uploadedfile import SimpleUploadedFile 5 | from django.utils.datastructures import MultiValueDict 6 | 7 | 8 | def transmute(f): 9 | if hasattr(f, "filename") and f.filename: 10 | v = SimpleUploadedFile(f.filename, f.value, f.type) 11 | else: 12 | v = f.value.decode("utf-8") 13 | return v 14 | 15 | 16 | def decode_fs(fs): 17 | POST, FILES = {}, {} 18 | for k in fs.keys(): 19 | v = transmute(fs[k]) 20 | if isinstance(v, SimpleUploadedFile): 21 | FILES[k] = [v] 22 | else: 23 | # Distutils sends UNKNOWN for empty fields (e.g platform) 24 | # [russell.sim@gmail.com] 25 | if v == "UNKNOWN": 26 | v = None 27 | POST[k] = [v] 28 | return MultiValueDict(POST), MultiValueDict(FILES) 29 | 30 | 31 | def debug(func): 32 | # @debug is handy when debugging distutils requests 33 | def _wrapped(*args, **kwargs): 34 | try: 35 | return func(*args, **kwargs) 36 | except: 37 | traceback.print_exception(*sys.exc_info()) 38 | return _wrapped -------------------------------------------------------------------------------- /djangopypi/views/__init__.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | from django.shortcuts import render_to_response 3 | from django.template import RequestContext 4 | 5 | from djangopypi.models import Project, Release 6 | from djangopypi.http import HttpResponseNotImplemented 7 | from djangopypi.http import parse_distutils_request 8 | from djangopypi.views.dists import register_or_upload 9 | from djangopypi.views.users import create_user 10 | from djangopypi.views.search import search 11 | 12 | 13 | ACTIONS = { 14 | # file_upload is the action used with distutils ``sdist`` command. 15 | "file_upload": register_or_upload, 16 | 17 | # submit is the :action used with distutils ``register`` command. 18 | "submit": register_or_upload, 19 | 20 | # user is the action used when registering a new user 21 | "user": create_user, 22 | } 23 | 24 | 25 | def simple(request, template_name="djangopypi/simple.html"): 26 | if request.method == "POST": 27 | post_data, files = parse_distutils_request(request) 28 | action_name = post_data.get(":action") 29 | if action_name not in ACTIONS: 30 | return HttpResponseNotImplemented( 31 | "The action %s is not implemented" % action_name) 32 | return ACTIONS[action_name](request, post_data, files) 33 | 34 | dists = Project.objects.all().order_by("name") 35 | context = RequestContext(request, { 36 | "dists": dists, 37 | "title": 'Package Index', 38 | }) 39 | 40 | return render_to_response(template_name, context_instance=context) 41 | 42 | 43 | def show_links(request, dist_name, 44 | template_name="djangopypi/show_links.html"): 45 | try: 46 | project = Project.objects.get(name=dist_name) 47 | releases = project.releases.all().order_by('-version') 48 | except Project.DoesNotExist: 49 | raise Http404 50 | 51 | context = RequestContext(request, { 52 | "dist_name": dist_name, 53 | "releases": releases, 54 | "project": project, 55 | "title": project.name, 56 | }) 57 | 58 | return render_to_response(template_name, context_instance=context) 59 | 60 | 61 | def show_version(request, dist_name, version, 62 | template_name="djangopypi/show_version.html"): 63 | try: 64 | project = Project.objects.get(name=dist_name) 65 | release = project.releases.get(version=version) 66 | except (Project.DoesNotExist, Release.DoesNotExist): 67 | raise Http404() 68 | 69 | context = RequestContext(request, { 70 | "dist_name": dist_name, 71 | "version": version, 72 | "release": release, 73 | "title": dist_name, 74 | }) 75 | 76 | return render_to_response(template_name, context_instance=context) 77 | -------------------------------------------------------------------------------- /djangopypi/views/dists.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings 4 | from django.http import (HttpResponse, HttpResponseForbidden, 5 | HttpResponseBadRequest) 6 | from django.utils.translation import ugettext_lazy as _ 7 | from django.contrib.auth import login 8 | 9 | from djangopypi.http import login_basic_auth, HttpResponseUnauthorized 10 | from djangopypi.forms import ProjectForm, ReleaseForm 11 | from djangopypi.models import Project, Release, Classifier, UPLOAD_TO 12 | 13 | ALREADY_EXISTS_FMT = _( 14 | "A file named '%s' already exists for %s. Please create a new release.") 15 | 16 | 17 | def submit_project_or_release(user, post_data, files): 18 | """Registers/updates a project or release""" 19 | try: 20 | project = Project.objects.get(name=post_data['name']) 21 | if project.owner != user: 22 | return HttpResponseForbidden( 23 | "That project is owned by someone else!") 24 | except Project.DoesNotExist: 25 | project = None 26 | 27 | project_form = ProjectForm(post_data, instance=project) 28 | if project_form.is_valid(): 29 | project = project_form.save(commit=False) 30 | project.owner = user 31 | project.save() 32 | for c in post_data.getlist('classifiers'): 33 | classifier, created = Classifier.objects.get_or_create(name=c) 34 | project.classifiers.add(classifier) 35 | if files: 36 | allow_overwrite = getattr(settings, 37 | "DJANGOPYPI_ALLOW_VERSION_OVERWRITE", False) 38 | try: 39 | release = Release.objects.get(version=post_data['version'], 40 | project=project, 41 | distribution=UPLOAD_TO + '/' + 42 | files['distribution']._name) 43 | if not allow_overwrite: 44 | return HttpResponseForbidden(ALREADY_EXISTS_FMT % ( 45 | release.filename, release)) 46 | except Release.DoesNotExist: 47 | release = None 48 | 49 | # If the old file already exists, django will append a _ after the 50 | # filename, however with .tar.gz files django does the "wrong" 51 | # thing and saves it as project-0.1.2.tar_.gz. So remove it before 52 | # django sees anything. 53 | release_form = ReleaseForm(post_data, files, instance=release) 54 | if release_form.is_valid(): 55 | if release and os.path.exists(release.distribution.path): 56 | os.remove(release.distribution.path) 57 | release = release_form.save(commit=False) 58 | release.project = project 59 | release.save() 60 | else: 61 | return HttpResponseBadRequest( 62 | "ERRORS: %s" % release_form.errors) 63 | else: 64 | return HttpResponseBadRequest("ERRORS: %s" % project_form.errors) 65 | 66 | return HttpResponse() 67 | 68 | 69 | def register_or_upload(request, post_data, files): 70 | user = login_basic_auth(request) 71 | if not user: 72 | return HttpResponseUnauthorized('pypi') 73 | 74 | login(request, user) 75 | if not request.user.is_authenticated(): 76 | return HttpResponseForbidden( 77 | "Not logged in, or invalid username/password.") 78 | 79 | return submit_project_or_release(user, post_data, files) 80 | -------------------------------------------------------------------------------- /djangopypi/views/search.py: -------------------------------------------------------------------------------- 1 | from django.template import RequestContext 2 | from django.shortcuts import render_to_response 3 | from django.db.models.query import Q 4 | 5 | from djangopypi.models import Project 6 | 7 | 8 | def _search_query(q): 9 | return Q(name__contains=q) | Q(summary__contains=q) 10 | 11 | 12 | def search(request, template="djangopypi/search_results.html"): 13 | context = RequestContext(request, {"dists": None, "search_term": ""}) 14 | 15 | if request.method == "POST": 16 | search_term = context["search_term"] = request.POST.get("search_term") 17 | if search_term: 18 | query = _search_query(search_term) 19 | context["dists"] = Project.objects.filter(query) 20 | 21 | if context["dists"] is None: 22 | context["dists"] = Project.objects.all() 23 | 24 | return render_to_response(template, context_instance=context) 25 | -------------------------------------------------------------------------------- /djangopypi/views/users.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse, HttpResponseBadRequest 2 | 3 | from registration.forms import RegistrationForm 4 | from registration.backends import get_backend 5 | 6 | DEFAULT_BACKEND = "registration.backends.default.DefaultBackend" 7 | 8 | 9 | def create_user(request, post_data, files, backend_name=DEFAULT_BACKEND): 10 | """Create new user from a distutil client request""" 11 | form = RegistrationForm({"username": post_data["name"], 12 | "email": post_data["email"], 13 | "password1": post_data["password"], 14 | "password2": post_data["password"]}) 15 | if not form.is_valid(): 16 | # Dist Utils requires error msg in HTTP status: "HTTP/1.1 400 msg" 17 | # Which is HTTP/WSGI incompatible, so we're just returning a empty 400. 18 | return HttpResponseBadRequest() 19 | 20 | backend = get_backend(backend_name) 21 | if not backend.registration_allowed(request): 22 | return HttpResponseBadRequest() 23 | new_user = backend.register(request, **form.cleaned_data) 24 | return HttpResponse("OK\n", status=200, mimetype='text/plain') 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | ask/chishop @ GitHub 9 | 10 | 33 | 34 | 35 | 36 | 37 | Fork me on GitHub 38 | 39 |
40 | 41 |
42 | 43 | 44 | 45 | 46 |
47 | 48 |

chishop 49 | by ask

50 | 51 |
52 | Simple PyPI server written in Django. 53 |
54 | 55 |

Simple PyPI server written in Django. Supports register/upload new projects and releases using distutils and installation of distributions on server using easy_install/pip.

Dependencies

56 |

Django >= 1.0.2

57 |

Install

58 |

see README.

59 |

License

60 |

BSD

61 |

Authors

62 |

Ask Solem (askh@modwheel.net)

63 |

Contact

64 |

Ask Solem Hoel (ask@modwheel.net)

65 | 66 | 67 |

Download

68 |

69 | You can download this project in either 70 | zip or 71 | tar formats. 72 |

73 |

You can also clone the project with Git 74 | by running: 75 |

$ git clone git://github.com/ask/chishop
76 |

77 | 78 | 81 | 82 |
83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import codecs 5 | 6 | try: 7 | from setuptools import setup, find_packages 8 | except ImportError: 9 | from ez_setup import use_setuptools 10 | use_setuptools() 11 | from setuptools import setup, find_packages 12 | 13 | from distutils.command.install_data import install_data 14 | from distutils.command.install import INSTALL_SCHEMES 15 | import sys 16 | 17 | djangopypi = __import__('djangopypi', {}, {}, ['']) 18 | 19 | packages, data_files = [], [] 20 | root_dir = os.path.dirname(__file__) 21 | if root_dir != '': 22 | os.chdir(root_dir) 23 | djangopypi_dir = "djangopypi" 24 | 25 | def osx_install_data(install_data): 26 | def finalize_options(self): 27 | self.set_undefined_options("install", ("install_lib", "install_dir")) 28 | install_data.finalize_options(self) 29 | 30 | #if sys.platform == "darwin": 31 | # cmdclasses = {'install_data': osx_install_data} 32 | #else: 33 | # cmdclasses = {'install_data': install_data} 34 | 35 | 36 | def fullsplit(path, result=None): 37 | if result is None: 38 | result = [] 39 | head, tail = os.path.split(path) 40 | if head == '': 41 | return [tail] + result 42 | if head == path: 43 | return result 44 | return fullsplit(head, [tail] + result) 45 | 46 | 47 | for scheme in INSTALL_SCHEMES.values(): 48 | scheme['data'] = scheme['purelib'] 49 | 50 | 51 | for dirpath, dirnames, filenames in os.walk(djangopypi_dir): 52 | # Ignore dirnames that start with '.' 53 | for i, dirname in enumerate(dirnames): 54 | if dirname.startswith("."): del dirnames[i] 55 | for filename in filenames: 56 | if filename.endswith(".py"): 57 | packages.append('.'.join(fullsplit(dirpath))) 58 | else: 59 | data_files.append([dirpath, [os.path.join(dirpath, f) for f in 60 | filenames]]) 61 | setup( 62 | name='chishop', 63 | version=djangopypi.__version__, 64 | description='Simple PyPI server written in Django.', 65 | author='Ask Solem', 66 | author_email='askh@opera.com', 67 | packages=packages, 68 | url="http://ask.github.com/chishop", 69 | zip_safe=False, 70 | data_files=data_files, 71 | install_requires=[ 72 | 'django>=1.0', 73 | 'docutils', 74 | 'django-registration>0.7', 75 | ], 76 | classifiers=[ 77 | "Development Status :: 3 - Alpha", 78 | "Environment :: Web Environment", 79 | "Framework :: Django", 80 | "Operating System :: OS Independent", 81 | "Intended Audience :: Developers", 82 | "Intended Audience :: System Administrators", 83 | "License :: OSI Approved :: BSD License", 84 | "Topic :: System :: Software Distribution", 85 | "Programming Language :: Python", 86 | ], 87 | long_description=codecs.open('README', "r", "utf-8").read(), 88 | ) 89 | --------------------------------------------------------------------------------