diff --git a/.gitignore b/.gitignore index b0a673f..6327f8f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.project +.pydevproject +.settings .DS_Store *.pyc *~ @@ -15,4 +18,9 @@ parts eggs bin developer-eggs +develop-eggs downloads +.installed.cfg +chishop/media/dists/* +chishop/haystack/* +build/ diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..7e741f2 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,19 @@ +Authors/Contributors +-------------------- + +* Ask Solem +* Rune Halvorsen +* Russell Sim +* Brian Rosner +* Hugo Lopes Tavares +* Sverre Johansen +* Bo Shi +* Carl Meyer +* Vinícius das Chagas Silva +* Vanderson Mota dos Santos +* Stefan Foulis +* Michael Richardson +* Benjamin Liles +* Halldór Rúnarsson +* Jannis Leidel +* Sebastien Fievet diff --git a/Changelog b/Changelog deleted file mode 100644 index f148013..0000000 --- a/Changelog +++ /dev/null @@ -1,41 +0,0 @@ -============== -Change history -============== - -0.2.0 :date:`2009-03-22 1:21 A.M CET` :author:askh@opera.com --------------------------------------------------------------- - - Backwards incompatible changes - ------------------------------- - - * Projects now has an associated owner, so old projects - must be exported and imported to a new database. - - * Registering projects and uploading releases now requires - authentication. Use the admin interface to create new users, - registering users via distutils won't be available until - :version:`0.3.0`. - - * Every project now has an owner, so only the user registering the - project can add releases. - - * md5sum is now properly listed in the release link. - - * Project names can now have dots (`.`) in them. - - * Fixed a bug where filenames was mangled if the distribution file - already existed. If someone uploaded version `1.0` of project `grail` - twice, the new filename was renamed to `grail-1.0.tar_.gz`, and a - backup of the old release was kept. Pip couldn't handle these filenames, - so we delete the old release first. - - * Releases now list both project name and version, instead of just version - in the admin interface. - - * Added a sample buildout.cfg. Thanks to Rune Halvorsen (runeh@opera.com). - - -0.1.0 :date:`2009-03-22 1:21 A.M CET` :author:askh@opera.com --------------------------------------------------------------- - - * Initial release diff --git a/Changelog.rst b/Changelog.rst new file mode 100644 index 0000000..fc506d6 --- /dev/null +++ b/Changelog.rst @@ -0,0 +1,76 @@ +History +======= + +0.5 (2012-04) +------------- + +* Renamed fork to userpypi +* Added multi-user support with private indexes + +0.4.3 (2011-02-22) +------------------ + +* Moved xmlrpc views into views folder +* Moved xmlrpc command settings to the settings file +* Cleaned up xmlrpc views to remove django.contrib.sites dependency + +0.4.2 (2011-02-21) +------------------ + +* Added CSRF support for Django>=1.2 +* Added conditional support to proxy packages not indexed + +0.4.1 (2010-06-17) +------------------ + +* Added conditional support for django-haystack searching + +0.4 (2010-06-14) +---------------- + +* 'list_classifiers' action handler +* Issue #3: decorators imports incompatible with Django 1.0, 1.1 +* RSS support for release index, packages +* Distribution uploads (files for releases) + +0.3.1 (2010-06-09) +------------------ + +* Installation bugfix + +0.3 (2010-06-09) +---------------- + +* Added DOAP views of packages and releases +* Splitting userpypi off of chishop +* Switched most views to using django generic views + +Backwards incompatible changes +______________________________ + +* Refactored package/project model to support multiple owners/maintainers +* Refactored release to match the metadata only that exists on pypi.python.org +* Created a Distribution model for distribution files on a release + +0.2.0 (2009-03-22) +------------------ + +* Registering projects and uploading releases now requires authentication. +* Every project now has an owner, so only the user registering the project can + add releases. +* md5sum is now properly listed in the release link. +* Project names can now have dots ('.') in them. +* Fixed a bug where filenames was mangled if the distribution file already existed. +* Releases now list both project name and version, instead of just version in the admin interface. +* Added a sample buildout.cfg. Thanks to Rune Halvorsen (runeh@opera.com). + +Backwards incompatible changes +______________________________ + +* Projects now has an associated owner, so old projects must be exported and + imported to a new database. + +0.1.0 (2009-03-22) +------------------ + +* Initial release diff --git a/MANIFEST.in b/MANIFEST.in index b6c623c..8ff353b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,6 @@ -include AUTHORS -include README -include TODO -include MANIFEST.in +include README.rst +include AUTHORS.rst +include Changelog.rst include LICENSE -include Changelog -recursive-include djangopypi * -recursive-include chishop * +include TODO +# include MANIFEST.in \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..31f6cd0 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +.PHONY: run +run: bin/django syncdb + ./bin/django runserver 0.0.0.0:10000 + +.PHONY: shell +shell: bin/django + ./bin/django shell + +bin/django: bin/buildout buildout.cfg + ./bin/buildout + touch bin/django + +bin/buildout: + python bootstrap.py --distribute + +syncdb: bin/django + ./bin/django syncdb + touch syncdb + +buildout.cfg: diff --git a/README b/README deleted file mode 100644 index ca3b5e1..0000000 --- a/README +++ /dev/null @@ -1,79 +0,0 @@ -========================================= -ChiShop/DjangoPyPI -========================================= -:Version: 0.1 - -Installation -============ - -Install dependencies:: - - $ python bootstrap.py --distribute - $ ./bin/buildout - -Initial configuration ---------------------- -:: - - $ $EDITOR chishop/settings.py - $ ./bin/django syncdb - -Run the PyPI server -------------------- -:: - - $ ./bin/django runserver - -Please note that ``chishop/media/dists`` has to be writable by the -user the web-server is running as. - -In production -------------- - -You may want to copy the file ``chishop/production_example.py`` and modify -for use as your production settings; you will also need to modify -``bin/django.wsgi`` to refer to your production settings. - -Using Setuptools -================ - -Add the following to your ``~/.pypirc`` file:: - - [distutils] - index-servers = - pypi - local - - - [pypi] - username:user - password:secret - - [local] - - username:user - password:secret - repository:http://localhost:8000 - -Uploading a package: Python >=2.6 --------------------------------------------- - -To push the package to the local pypi:: - - $ python setup.py register -r local sdist upload -r local - - -Uploading a package: Python <2.6 -------------------------------------------- - -If you don't have Python 2.6 please run the command below to install the backport of the extension:: - - $ easy_install -U collective.dist - -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. - -To push the package to the local pypi:: - - $ python setup.py mregister -r local sdist mupload -r local - -.. # vim: syntax=rst expandtab tabstop=4 shiftwidth=4 shiftround diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..3ce48be --- /dev/null +++ b/README.rst @@ -0,0 +1,95 @@ +DjangoPyPI +========== + +DjangoPyPI is a Django application that provides a re-implementation of the +`Python Package Index `_. + +Installation +------------ + +Path +____ + +The first step is to get ``userpypi`` into your Python path. + +Buildout +++++++++ + +Simply add ``userpypi`` to your list of ``eggs`` and run buildout again it +should downloaded and installed properly. + +EasyInstall/Setuptools +++++++++++++++++++++++ + +If you have setuptools installed, you can use ``easy_install userpypi`` + +Manual +++++++ + +Download and unpack the source then run:: + + $ python setup.py install + +Django Settings +_______________ + +Add ``userpypi`` to your ``INSTALLED_APPS`` setting and run ``syncdb`` again +to get the database tables [#]_. + +Then add an include in your url config for ``userpypi.urls``:: + + urlpatterns = patterns("", + ... + url(r'', include("userpypi.urls")) + ) + +This will make the repository interface be accessible at ``/pypi/``. + + + +Uploading to your PyPI +---------------------- + +Assuming you are running your Django site locally for now, add the following to +your ``~/.pypirc`` file:: + + [distutils] + index-servers = + pypi + local + + [pypi] + username:user + password:secret + + [local] + username:user + password:secret + repository:http://localhost:8000/pypi + +Uploading a package: Python >=2.6 +_________________________________ + +To push the package to the local pypi:: + + $ python setup.py register -r local sdist upload -r local + + +Uploading a package: Python <2.6 +________________________________ + +If you don't have Python 2.6 please run the command below to install the +backport of the extension for multiple repositories:: + + $ easy_install -U collective.dist + +Instead of using register and dist command, you can use ``mregister`` and +``mupload`` which are a backport of python 2.6 register and upload commands +that supports multiple servers. + +To push the package to the local pypi:: + + $ python setup.py mregister -r local sdist mupload -r local + +.. [#] ``userpypi`` is South enabled, if you are using South then you will need + to run the South ``migrate`` command to get the tables. \ No newline at end of file diff --git a/TODO b/TODO deleted file mode 100644 index f8ed064..0000000 --- a/TODO +++ /dev/null @@ -1,51 +0,0 @@ -PyPI feature replication -======================== - -* Make it possible to register users via distutils. - There should be a setting to turn this feature on/off for private PyPIs. - [taken-by: sverrej] - -* Roles (co-owners/maintainers) - - One possible solution: - http://github.com/initcrash/django-object-permissions/tree - I'm not sure what the difference between a co-owner and maintainer is, - maybe it's just a label. -* Package author admin interface (submit, edit, view) -* Documentation upload -* Ratings -* Random Monty Python quotes :-) -* Comments :-) - -Post-PyPI -========= - -* PEP-381: Mirroring infrastructure for PyPI - [taken-by: jezdez] - -* API to submit test reports for smoke test bots. Like CPAN Testers. - Platform/version/matrix etc. - -* Different listings: Author listings, classifier listings, etc. - -* Search metadata - -* Automatic generation of Sphinx for modules (so you can view them directly -on pypi, like CPAN), Module listing etc. - -* Listing of special files: README, LICENSE, Changefile/Changes, TODO, - MANIFEST. - -* Dependency graphs. - -* Package file browser (like CPAN) - - - - -Documentation -============= - -* Write a tutorial on how to set up the server, registering projects, and - how to upload releases. - - diff --git a/bootstrap.py b/bootstrap.py deleted file mode 100644 index b964024..0000000 --- a/bootstrap.py +++ /dev/null @@ -1,121 +0,0 @@ -############################################################################## -# -# Copyright (c) 2006 Zope Corporation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Bootstrap a buildout-based project - -Simply run this script in a directory containing a buildout.cfg. -The script accepts buildout command-line options, so you can -use the -c option to specify an alternate configuration file. - -$Id$ -""" - -import os, shutil, sys, tempfile, urllib2 -from optparse import OptionParser - -tmpeggs = tempfile.mkdtemp() - -is_jython = sys.platform.startswith('java') - -# parsing arguments -parser = OptionParser() -parser.add_option("-v", "--version", dest="version", - help="use a specific zc.buildout version") -parser.add_option("-d", "--distribute", - action="store_true", dest="distribute", default=False, - help="Use Disribute rather than Setuptools.") - -parser.add_option("-c", None, action="store", dest="config_file", - help=("Specify the path to the buildout configuration " - "file to be used.")) - -options, args = parser.parse_args() - -# if -c was provided, we push it back into args for buildout' main function -if options.config_file is not None: - args += ['-c', options.config_file] - -if options.version is not None: - VERSION = '==%s' % options.version -else: - VERSION = '' - -USE_DISTRIBUTE = options.distribute -args = args + ['bootstrap'] - -to_reload = False -try: - import pkg_resources - if not hasattr(pkg_resources, '_distribute'): - to_reload = True - raise ImportError -except ImportError: - ez = {} - if USE_DISTRIBUTE: - exec urllib2.urlopen('http://python-distribute.org/distribute_setup.py' - ).read() in ez - ez['use_setuptools'](to_dir=tmpeggs, download_delay=0, no_fake=True) - else: - exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py' - ).read() in ez - ez['use_setuptools'](to_dir=tmpeggs, download_delay=0) - - if to_reload: - reload(pkg_resources) - else: - import pkg_resources - -if sys.platform == 'win32': - def quote(c): - if ' ' in c: - return '"%s"' % c # work around spawn lamosity on windows - else: - return c -else: - def quote (c): - return c - -cmd = 'from setuptools.command.easy_install import main; main()' -ws = pkg_resources.working_set - -if USE_DISTRIBUTE: - requirement = 'distribute' -else: - requirement = 'setuptools' - -if is_jython: - import subprocess - - assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd', - quote(tmpeggs), 'zc.buildout' + VERSION], - env=dict(os.environ, - PYTHONPATH= - ws.find(pkg_resources.Requirement.parse(requirement)).location - ), - ).wait() == 0 - -else: - assert os.spawnle( - os.P_WAIT, sys.executable, quote (sys.executable), - '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout' + VERSION, - dict(os.environ, - PYTHONPATH= - ws.find(pkg_resources.Requirement.parse(requirement)).location - ), - ) == 0 - -ws.add_entry(tmpeggs) -ws.require('zc.buildout' + VERSION) -import zc.buildout.buildout -zc.buildout.buildout.main(args) -shutil.rmtree(tmpeggs) diff --git a/buildout.cfg b/buildout.cfg index 160db4c..33463b9 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -7,7 +7,6 @@ eggs = pkginfo [django] recipe = djangorecipe -version = 1.1.1 settings = settings eggs = ${buildout:eggs} test = djangopypi diff --git a/chishop/__init__.py b/chishop/__init__.py deleted file mode 100644 index b56a51c..0000000 --- a/chishop/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -VERSION = (0, 1, 1) -__version__ = ".".join(map(str, VERSION)) diff --git a/chishop/conf/default.py b/chishop/conf/default.py index ce43d48..366aaa8 100644 --- a/chishop/conf/default.py +++ b/chishop/conf/default.py @@ -70,9 +70,9 @@ # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.load_template_source', - 'django.template.loaders.app_directories.load_template_source', -# 'django.template.loaders.eggs.load_template_source', + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + #'django.template.loaders.eggs.load_template_source', ) MIDDLEWARE_CLASSES = ( @@ -84,7 +84,7 @@ ROOT_URLCONF = 'chishop.urls' TEMPLATE_CONTEXT_PROCESSORS = ( - "django.core.context_processors.auth", + "django.contrib.auth.context_processors.auth", "django.core.context_processors.debug", "django.core.context_processors.i18n", "django.core.context_processors.media", diff --git a/chishop/manage.py b/chishop/manage.py deleted file mode 100644 index 5e78ea9..0000000 --- a/chishop/manage.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python -from django.core.management import execute_manager -try: - import settings # Assumed to be in the same directory. -except ImportError: - import sys - 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__) - sys.exit(1) - -if __name__ == "__main__": - execute_manager(settings) diff --git a/chishop/media/style/djangopypi.css b/chishop/media/style/djangopypi.css deleted file mode 100644 index e6fbfd9..0000000 --- a/chishop/media/style/djangopypi.css +++ /dev/null @@ -1,4 +0,0 @@ -.search { - text-align:right; - margin-right: 10px; -} \ No newline at end of file diff --git a/chishop/production_example.py b/chishop/production_example.py deleted file mode 100644 index b64623e..0000000 --- a/chishop/production_example.py +++ /dev/null @@ -1,18 +0,0 @@ -from conf.default import * -import os - -DEBUG = False -TEMPLATE_DEBUG = DEBUG - -ADMINS = ( - ('chishop', 'example@example.org'), -) - -MANAGERS = ADMINS - -DATABASE_ENGINE = 'postgresql_psycopg2' -DATABASE_NAME = 'chishop' -DATABASE_USER = 'chishop' -DATABASE_PASSWORD = 'chishop' -DATABASE_HOST = '' -DATABASE_PORT = '' diff --git a/chishop/settings.py b/chishop/settings.py index e68a4e5..5948446 100644 --- a/chishop/settings.py +++ b/chishop/settings.py @@ -15,9 +15,13 @@ MANAGERS = ADMINS -DATABASE_ENGINE = 'sqlite3' -DATABASE_NAME = os.path.join(here, 'devdatabase.db') -DATABASE_USER = '' -DATABASE_PASSWORD = '' -DATABASE_HOST = '' -DATABASE_PORT = '' +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'dev.sqlite3', # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} diff --git a/chishop/templates/404.html b/chishop/templates/404.html deleted file mode 100644 index 9bf4293..0000000 --- a/chishop/templates/404.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "admin/base_site.html" %} -{% load i18n %} - -{% block title %}{% trans 'Page not found' %}{% endblock %} - -{% block content %} - -

{% trans 'Page not found' %}

- -

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

- -{% endblock %} diff --git a/chishop/templates/500.html b/chishop/templates/500.html deleted file mode 100644 index b30e431..0000000 --- a/chishop/templates/500.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "admin/base_site.html" %} -{% load i18n %} - -{% block breadcrumbs %}{% endblock %} - -{% block title %}{% trans 'Server error (500)' %}{% endblock %} - -{% block content %} -

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

-

{% 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." %}

- -{% endblock %} diff --git a/chishop/templates/admin/base_site.html b/chishop/templates/admin/base_site.html deleted file mode 100644 index 9443eb2..0000000 --- a/chishop/templates/admin/base_site.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "admin/base.html" %} -{% load i18n %} - -{% block title %}{{ title }} | {% trans 'Chishop site admin' %}{% endblock %} - -{% block branding %} -

{% trans 'Chishop' %}

-{% endblock %} - -{% block nav-global %}{% endblock %} diff --git a/chishop/templates/base_site.html b/chishop/templates/base_site.html deleted file mode 100644 index 5257255..0000000 --- a/chishop/templates/base_site.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{{ title }} | Chishop{% endblock %} - -{% block bread_crumbs_1 %} -› {{ title }} -{% endblock %} -{% block site_name_header %} -Chishop -{% endblock %} - -{% block content_title %}{{ title }}{% endblock %} - -{% block nav-global %}{% endblock %} diff --git a/chishop/templates/djangopypi/pypi.html b/chishop/templates/djangopypi/pypi.html deleted file mode 100644 index f98cf44..0000000 --- a/chishop/templates/djangopypi/pypi.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "base_site.html" %} - -{% block bread_crumbs_1 %}{% endblock %} - -{% block content %} - - -{% for dist in dists %} - - - - -{% endfor %} -
UpdatedPackageSummary
{{ dist.updated|date:"d/m/y" }} - {{ dist.name }}{{ dist.summary|truncatewords:10 }}
-{% endblock %} diff --git a/chishop/templates/djangopypi/pypi_show_links.html b/chishop/templates/djangopypi/pypi_show_links.html deleted file mode 100644 index 6e46635..0000000 --- a/chishop/templates/djangopypi/pypi_show_links.html +++ /dev/null @@ -1,39 +0,0 @@ -{% extends "base_site.html" %} - -{% block content %} -

-{{ project.summary }} -

-{% load safemarkup %} -{{ project.description|saferst }} - -
- - - {% for release in releases %} - - - - - - - - - {% endfor %} -
FilenamePlatformTypeVersionUploaded OnSize
{{ release.filename }}{{ release.platform }}{{ release.type }}{{ release.version }}{{ release.upload_time }}{{ release.distribution.size|filesizeformat }}
-
- -{% endblock %} - diff --git a/chishop/templates/djangopypi/search_results.html b/chishop/templates/djangopypi/search_results.html deleted file mode 100644 index dccf61f..0000000 --- a/chishop/templates/djangopypi/search_results.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends "base_site.html" %} - -{% block bread_crumbs_1 %}›Search{% endblock %} - -{% block content %} - {% ifnotequal search_term ''%} -

Index of Packages Matching '{{ search_term }}'

- {% else %} -

You need to supply a search term.

- {% endifnotequal %} - {% if dists %} - - - - - - - - {% for dist in dists %} - - - - - {% endfor %} - -
UpdatedPackageSummary
{{ dist.updated|date:"d/m/y" }} - {{ dist.name }}{{ dist.summary|truncatewords:10 }}
- {% else %} - There were no matches. - {% endif %} -{% endblock content %} \ No newline at end of file diff --git a/chishop/templates/djangopypi/show_links.html b/chishop/templates/djangopypi/show_links.html deleted file mode 100644 index 57a1268..0000000 --- a/chishop/templates/djangopypi/show_links.html +++ /dev/null @@ -1,7 +0,0 @@ -Links for {{ dist_name }} -

Links for {{ dist_name }}

- -{% for release in releases %} -{{ release.filename }}
-{% endfor %} - diff --git a/chishop/templates/djangopypi/show_version.html b/chishop/templates/djangopypi/show_version.html deleted file mode 100644 index d3b393a..0000000 --- a/chishop/templates/djangopypi/show_version.html +++ /dev/null @@ -1,5 +0,0 @@ -Links for {{ dist_name }} {{ version }} -

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

- -{{ release.filename }}
- diff --git a/chishop/templates/djangopypi/simple.html b/chishop/templates/djangopypi/simple.html deleted file mode 100644 index 6a21d5c..0000000 --- a/chishop/templates/djangopypi/simple.html +++ /dev/null @@ -1,5 +0,0 @@ -Simple Index -{% for dist in dists %} -{{ dist.name }}
-{% endfor %} - diff --git a/chishop/templates/registration/activate.html b/chishop/templates/registration/activate.html deleted file mode 100644 index bc67771..0000000 --- a/chishop/templates/registration/activate.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base_site.html" %} - -{% block content %} -

Activation Failed

-

- Activation with key {{activation_key}} failed. -

-{% endblock %} diff --git a/chishop/templates/registration/activation_complete.html b/chishop/templates/registration/activation_complete.html deleted file mode 100644 index c8e8aca..0000000 --- a/chishop/templates/registration/activation_complete.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends "base_site.html" %} - -{% block content %} -

Activation complete.

-

- Hello {{user}}, you are registered. - Go here to get back to the main page. -

-{% endblock %} diff --git a/chishop/templates/registration/activation_email.txt b/chishop/templates/registration/activation_email.txt deleted file mode 100644 index 0a25329..0000000 --- a/chishop/templates/registration/activation_email.txt +++ /dev/null @@ -1,6 +0,0 @@ -Welcome to Chishop. - -Please click here to activate your account: -http://{{site}}/accounts/activate/{{activation_key}}/ - -Account has to be activated within {{expiration_days}} days. diff --git a/chishop/templates/registration/activation_email_subject.txt b/chishop/templates/registration/activation_email_subject.txt deleted file mode 100644 index 93618cc..0000000 --- a/chishop/templates/registration/activation_email_subject.txt +++ /dev/null @@ -1 +0,0 @@ -Account Activation - {{ site }} diff --git a/chishop/templates/registration/login.html b/chishop/templates/registration/login.html deleted file mode 100644 index 6c7f799..0000000 --- a/chishop/templates/registration/login.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base_site.html" %} - -{% block content %} -
- {{form.as_p}} - -
-{% endblock %} diff --git a/chishop/templates/registration/logout.html b/chishop/templates/registration/logout.html deleted file mode 100644 index 06483a8..0000000 --- a/chishop/templates/registration/logout.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "base_site.html" %} - -{% block main_content %} -

- {%trans "Logged out."%} -

-{% endblock %} diff --git a/chishop/templates/registration/registration_closed.html b/chishop/templates/registration/registration_closed.html deleted file mode 100644 index c92e80b..0000000 --- a/chishop/templates/registration/registration_closed.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base_site.html" %} - -{% block content %} -

Registration Closed

-

- Registration is disabled. -

-{% endblock %} diff --git a/chishop/templates/registration/registration_complete.html b/chishop/templates/registration/registration_complete.html deleted file mode 100644 index d9a19cf..0000000 --- a/chishop/templates/registration/registration_complete.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base_site.html" %} - -{% block content %} -

Registration complete

-

- An activation mail has been sent to you. -

-{% endblock %} diff --git a/chishop/templates/registration/registration_form.html b/chishop/templates/registration/registration_form.html deleted file mode 100644 index 719a875..0000000 --- a/chishop/templates/registration/registration_form.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base_site.html" %} - -{% block content %} -

Register

-
- {{form.as_p}} - -{% endblock %} diff --git a/chishop/urls.py b/chishop/urls.py index 5a5dd77..7046fcb 100644 --- a/chishop/urls.py +++ b/chishop/urls.py @@ -16,7 +16,7 @@ urlpatterns += patterns("", # Admin interface url(r'^admin/doc/', include("django.contrib.admindocs.urls")), - url(r'^admin/(.*)', admin.site.root), + url(r'^admin/', include(admin.site.urls)), # Registration url(r'^accounts/', include('registration.backends.default.urls')), diff --git a/djangopypi/__init__.py b/djangopypi/__init__.py deleted file mode 100644 index b32a761..0000000 --- a/djangopypi/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -VERSION = (0, 2, 0) -__version__ = ".".join(map(str, VERSION)) diff --git a/djangopypi/admin.py b/djangopypi/admin.py deleted file mode 100644 index 6f994e8..0000000 --- a/djangopypi/admin.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.contrib import admin -from djangopypi.models import Project, Release, Classifier - -admin.site.register(Project) -admin.site.register(Release) -admin.site.register(Classifier) diff --git a/djangopypi/forms.py b/djangopypi/forms.py deleted file mode 100644 index 6a65d37..0000000 --- a/djangopypi/forms.py +++ /dev/null @@ -1,17 +0,0 @@ -import os -from django import forms -from django.conf import settings -from djangopypi.models import Project, Classifier, Release -from django.utils.translation import ugettext_lazy as _ - - -class ProjectForm(forms.ModelForm): - class Meta: - model = Project - exclude = ['owner', 'classifiers'] - - -class ReleaseForm(forms.ModelForm): - class Meta: - model = Release - exclude = ['project'] \ No newline at end of file diff --git a/djangopypi/http.py b/djangopypi/http.py deleted file mode 100644 index 7be5959..0000000 --- a/djangopypi/http.py +++ /dev/null @@ -1,66 +0,0 @@ -from django.http import HttpResponse -from django.core.files.uploadedfile import SimpleUploadedFile -from django.utils.datastructures import MultiValueDict -from django.contrib.auth import authenticate - - -class HttpResponseNotImplemented(HttpResponse): - status_code = 501 - - -class HttpResponseUnauthorized(HttpResponse): - status_code = 401 - - def __init__(self, realm): - HttpResponse.__init__(self) - self['WWW-Authenticate'] = 'Basic realm="%s"' % realm - - -def parse_distutils_request(request): - raw_post_data = request.raw_post_data - sep = raw_post_data.splitlines()[1] - items = raw_post_data.split(sep) - post_data = {} - files = {} - for part in filter(lambda e: not e.isspace(), items): - item = part.splitlines() - if len(item) < 2: - continue - header = item[1].replace("Content-Disposition: form-data; ", "") - kvpairs = header.split(";") - headers = {} - for kvpair in kvpairs: - if not kvpair: - continue - key, value = kvpair.split("=") - headers[key] = value.strip('"') - if "name" not in headers: - continue - content = part[len("\n".join(item[0:2]))+2:len(part)-1] - if "filename" in headers: - file = SimpleUploadedFile(headers["filename"], content, - content_type="application/gzip") - files["distribution"] = [file] - elif headers["name"] in post_data: - post_data[headers["name"]].append(content) - else: - # Distutils sends UNKNOWN for empty fields (e.g platform) - # [russell.sim@gmail.com] - if content == 'UNKNOWN': - post_data[headers["name"]] = [None] - else: - post_data[headers["name"]] = [content] - - return MultiValueDict(post_data), MultiValueDict(files) - - -def login_basic_auth(request): - authentication = request.META.get("HTTP_AUTHORIZATION") - if not authentication: - return - (authmeth, auth) = authentication.split(' ', 1) - if authmeth.lower() != "basic": - return - auth = auth.strip().decode("base64") - username, password = auth.split(":", 1) - return authenticate(username=username, password=password) diff --git a/djangopypi/management/commands/__init__.py b/djangopypi/management/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/djangopypi/models.py b/djangopypi/models.py deleted file mode 100644 index d98c4da..0000000 --- a/djangopypi/models.py +++ /dev/null @@ -1,132 +0,0 @@ -import os -from django.conf import settings -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth.models import User - -OS_NAMES = ( - ("aix", "AIX"), - ("beos", "BeOS"), - ("debian", "Debian Linux"), - ("dos", "DOS"), - ("freebsd", "FreeBSD"), - ("hpux", "HP/UX"), - ("mac", "Mac System x."), - ("macos", "MacOS X"), - ("mandrake", "Mandrake Linux"), - ("netbsd", "NetBSD"), - ("openbsd", "OpenBSD"), - ("qnx", "QNX"), - ("redhat", "RedHat Linux"), - ("solaris", "SUN Solaris"), - ("suse", "SuSE Linux"), - ("yellowdog", "Yellow Dog Linux"), -) - -ARCHITECTURES = ( - ("alpha", "Alpha"), - ("hppa", "HPPA"), - ("ix86", "Intel"), - ("powerpc", "PowerPC"), - ("sparc", "Sparc"), - ("ultrasparc", "UltraSparc"), -) - -UPLOAD_TO = getattr(settings, - "DJANGOPYPI_RELEASE_UPLOAD_TO", 'dist') - -class Classifier(models.Model): - name = models.CharField(max_length=255, unique=True) - - class Meta: - verbose_name = _(u"classifier") - verbose_name_plural = _(u"classifiers") - - def __unicode__(self): - return self.name - - -class Project(models.Model): - name = models.CharField(max_length=255, unique=True) - license = models.TextField(blank=True) - metadata_version = models.CharField(max_length=64, default=1.0) - author = models.CharField(max_length=128, blank=True) - home_page = models.URLField(verify_exists=False, blank=True, null=True) - download_url = models.CharField(max_length=200, blank=True, null=True) - summary = models.TextField(blank=True) - description = models.TextField(blank=True) - author_email = models.CharField(max_length=255, blank=True) - classifiers = models.ManyToManyField(Classifier) - owner = models.ForeignKey(User, related_name="projects") - updated = models.DateTimeField(auto_now=True) - - class Meta: - verbose_name = _(u"project") - verbose_name_plural = _(u"projects") - - def __unicode__(self): - return self.name - - @models.permalink - def get_absolute_url(self): - return ('djangopypi-show_links', (), {'dist_name': self.name}) - - @models.permalink - def get_pypi_absolute_url(self): - return ('djangopypi-pypi_show_links', (), {'dist_name': self.name}) - - def get_release(self, version): - """Return the release object for version, or None""" - try: - return self.releases.get(version=version) - except Release.DoesNotExist: - return None - -class Release(models.Model): - version = models.CharField(max_length=32) - distribution = models.FileField(upload_to=UPLOAD_TO) - md5_digest = models.CharField(max_length=255, blank=True) - platform = models.CharField(max_length=128, blank=True) - signature = models.CharField(max_length=128, blank=True) - filetype = models.CharField(max_length=255, blank=True) - pyversion = models.CharField(max_length=32, blank=True) - project = models.ForeignKey(Project, related_name="releases") - upload_time = models.DateTimeField(auto_now=True) - - class Meta: - verbose_name = _(u"release") - verbose_name_plural = _(u"releases") - unique_together = ("project", "version", "platform", "distribution", "pyversion") - - def __unicode__(self): - return u"%s (%s)" % (self.release_name, self.platform) - - @property - def type(self): - dist_file_types = { - 'sdist':'Source', - 'bdist_dumb':'"dumb" binary', - 'bdist_rpm':'RPM', - 'bdist_wininst':'MS Windows installer', - 'bdist_egg':'Python Egg', - 'bdist_dmg':'OS X Disk Image'} - return dist_file_types.get(self.filetype, self.filetype) - - @property - def filename(self): - return os.path.basename(self.distribution.name) - - @property - def release_name(self): - return u"%s-%s" % (self.project.name, self.version) - - @property - def path(self): - return self.distribution.name - - @models.permalink - def get_absolute_url(self): - return ('djangopypi-show_version', (), {'dist_name': self.project, 'version': self.version}) - - def get_dl_url(self): - return "%s#md5=%s" % (self.distribution.url, self.md5_digest) diff --git a/djangopypi/templatetags/__init__.py b/djangopypi/templatetags/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/djangopypi/tests.py b/djangopypi/tests.py deleted file mode 100644 index 91682ae..0000000 --- a/djangopypi/tests.py +++ /dev/null @@ -1,139 +0,0 @@ -import unittest -import StringIO -from djangopypi.views import parse_distutils_request, simple -from djangopypi.models import Project, Classifier -from django.test.client import Client -from django.core.urlresolvers import reverse -from django.contrib.auth.models import User -from django.http import HttpRequest - -def create_post_data(action): - data = { - ":action": action, - "metadata_version": "1.0", - "name": "foo", - "version": "0.1.0-pre2", - "summary": "The quick brown fox jumps over the lazy dog.", - "home_page": "http://example.com", - "author": "Foo Bar Baz", - "author_email": "foobarbaz@example.com", - "license": "Apache", - "keywords": "foo bar baz", - "platform": "UNKNOWN", - "classifiers": [ - "Development Status :: 3 - Alpha", - "Environment :: Web Environment", - "Framework :: Django", - "Operating System :: OS Independent", - "Intended Audience :: Developers", - "Intended Audience :: System Administrators", - "License :: OSI Approved :: BSD License", - "Topic :: System :: Software Distribution", - "Programming Language :: Python", - ], - "download_url": "", - "provides": "", - "requires": "", - "obsoletes": "", - "description": """ -========= -FOOBARBAZ -========= - -Introduction ------------- - ``foo`` :class:`bar` - *baz* - [foaoa] - """, - } - return data - -def create_request(data): - boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' - sep_boundary = '\n--' + boundary - end_boundary = sep_boundary + '--' - body = StringIO.StringIO() - for key, value in data.items(): - # handle multiple entries for the same name - if type(value) not in (type([]), type( () )): - value = [value] - for value in value: - value = unicode(value).encode("utf-8") - body.write(sep_boundary) - body.write('\nContent-Disposition: form-data; name="%s"'%key) - body.write("\n\n") - body.write(value) - if value and value[-1] == '\r': - body.write('\n') # write an extra newline (lurve Macs) - body.write(end_boundary) - body.write("\n") - - return body.getvalue() - - -class MockRequest(object): - - def __init__(self, raw_post_data): - self.raw_post_data = raw_post_data - self.META = {} - - -class TestParseWeirdPostData(unittest.TestCase): - - def test_weird_post_data(self): - data = create_post_data("submit") - raw_post_data = create_request(data) - post, files = parse_distutils_request(MockRequest(raw_post_data)) - self.assertTrue(post) - - for key in post.keys(): - if isinstance(data[key], list): - self.assertEquals(data[key], post.getlist(key)) - elif data[key] == "UNKNOWN": - self.assertTrue(post[key] is None) - else: - self.assertEquals(post[key], data[key]) - - - -client = Client() - -class TestSearch(unittest.TestCase): - - def setUp(self): - dummy_user = User.objects.create(username='krill', password='12345', - email='krill@opera.com') - Project.objects.create(name='foo', license='Gnu', - summary="The quick brown fox jumps over the lazy dog.", - owner=dummy_user) - - def test_search_for_package(self): - response = client.post(reverse('djangopypi-search'), {'search_term': 'foo'}) - self.assertTrue("The quick brown fox jumps over the lazy dog." in response.content) - -class TestSimpleView(unittest.TestCase): - - def create_distutils_httprequest(self, user_data={}): - self.post_data = create_post_data(action='user') - self.post_data.update(user_data) - self.raw_post_data = create_request(self.post_data) - request = HttpRequest() - request.POST = self.post_data - request.method = "POST" - request.raw_post_data = self.raw_post_data - return request - - def test_user_registration(self): - request = self.create_distutils_httprequest({'name': 'peter_parker', 'email':'parker@dailybugle.com', - 'password':'spiderman'}) - response = simple(request) - self.assertEquals(200, response.status_code) - - def test_user_registration_with_wrong_data(self): - request = self.create_distutils_httprequest({'name': 'peter_parker', 'email':'parker@dailybugle.com', - 'password':'',}) - response = simple(request) - self.assertEquals(400, response.status_code) - - \ No newline at end of file diff --git a/djangopypi/urls.py b/djangopypi/urls.py deleted file mode 100644 index 79b16be..0000000 --- a/djangopypi/urls.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -from django.conf.urls.defaults import patterns, url, include - -urlpatterns = patterns("djangopypi.views", - # Simple PyPI - url(r'^simple/$', "simple", - name="djangopypi-simple"), - - url(r'^simple/(?P[\w\d_\.\-]+)/(?P[\w\.\d\-_]+)/$', - "show_version", - name="djangopypi-show_version"), - - url(r'^simple/(?P[\w\d_\.\-]+)/$', "show_links", - name="djangopypi-show_links"), - - url(r'^$', "simple", {'template_name': 'djangopypi/pypi.html'}, - name="djangopypi-pypi"), - - url(r'^(?P[\w\d_\.\-]+)/$', "show_links", - {'template_name': 'djangopypi/pypi_show_links.html'}, - name="djangopypi-pypi_show_links"), - - url(r'^search','search',name='djangopypi-search') -) \ No newline at end of file diff --git a/djangopypi/utils.py b/djangopypi/utils.py deleted file mode 100644 index ba2013e..0000000 --- a/djangopypi/utils.py +++ /dev/null @@ -1,38 +0,0 @@ -import sys -import traceback - -from django.core.files.uploadedfile import SimpleUploadedFile -from django.utils.datastructures import MultiValueDict - - -def transmute(f): - if hasattr(f, "filename") and f.filename: - v = SimpleUploadedFile(f.filename, f.value, f.type) - else: - v = f.value.decode("utf-8") - return v - - -def decode_fs(fs): - POST, FILES = {}, {} - for k in fs.keys(): - v = transmute(fs[k]) - if isinstance(v, SimpleUploadedFile): - FILES[k] = [v] - else: - # Distutils sends UNKNOWN for empty fields (e.g platform) - # [russell.sim@gmail.com] - if v == "UNKNOWN": - v = None - POST[k] = [v] - return MultiValueDict(POST), MultiValueDict(FILES) - - -def debug(func): - # @debug is handy when debugging distutils requests - def _wrapped(*args, **kwargs): - try: - return func(*args, **kwargs) - except: - traceback.print_exception(*sys.exc_info()) - return _wrapped \ No newline at end of file diff --git a/djangopypi/views/__init__.py b/djangopypi/views/__init__.py deleted file mode 100644 index 1438c54..0000000 --- a/djangopypi/views/__init__.py +++ /dev/null @@ -1,76 +0,0 @@ -from django.http import Http404 -from django.shortcuts import render_to_response -from django.template import RequestContext - -from djangopypi.models import Project, Release -from djangopypi.http import HttpResponseNotImplemented -from djangopypi.http import parse_distutils_request -from djangopypi.views.dists import register_or_upload -from djangopypi.views.users import create_user -from djangopypi.views.search import search - - -ACTIONS = { - # file_upload is the action used with distutils ``sdist`` command. - "file_upload": register_or_upload, - - # submit is the :action used with distutils ``register`` command. - "submit": register_or_upload, - - # user is the action used when registering a new user - "user": create_user, -} - - -def simple(request, template_name="djangopypi/simple.html"): - if request.method == "POST": - post_data, files = parse_distutils_request(request) - action_name = post_data.get(":action") - if action_name not in ACTIONS: - return HttpResponseNotImplemented( - "The action %s is not implemented" % action_name) - return ACTIONS[action_name](request, post_data, files) - - dists = Project.objects.all().order_by("name") - context = RequestContext(request, { - "dists": dists, - "title": 'Package Index', - }) - - return render_to_response(template_name, context_instance=context) - - -def show_links(request, dist_name, - template_name="djangopypi/show_links.html"): - try: - project = Project.objects.get(name=dist_name) - releases = project.releases.all().order_by('-version') - except Project.DoesNotExist: - raise Http404 - - context = RequestContext(request, { - "dist_name": dist_name, - "releases": releases, - "project": project, - "title": project.name, - }) - - return render_to_response(template_name, context_instance=context) - - -def show_version(request, dist_name, version, - template_name="djangopypi/show_version.html"): - try: - project = Project.objects.get(name=dist_name) - release = project.releases.get(version=version) - except (Project.DoesNotExist, Release.DoesNotExist): - raise Http404() - - context = RequestContext(request, { - "dist_name": dist_name, - "version": version, - "release": release, - "title": dist_name, - }) - - return render_to_response(template_name, context_instance=context) diff --git a/djangopypi/views/dists.py b/djangopypi/views/dists.py deleted file mode 100644 index 9e4a146..0000000 --- a/djangopypi/views/dists.py +++ /dev/null @@ -1,79 +0,0 @@ -import os - -from django.conf import settings -from django.http import (HttpResponse, HttpResponseForbidden, - HttpResponseBadRequest) -from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth import login - -from djangopypi.http import login_basic_auth, HttpResponseUnauthorized -from djangopypi.forms import ProjectForm, ReleaseForm -from djangopypi.models import Project, Release, Classifier, UPLOAD_TO - -ALREADY_EXISTS_FMT = _( - "A file named '%s' already exists for %s. Please create a new release.") - - -def submit_project_or_release(user, post_data, files): - """Registers/updates a project or release""" - try: - project = Project.objects.get(name=post_data['name']) - if project.owner != user: - return HttpResponseForbidden( - "That project is owned by someone else!") - except Project.DoesNotExist: - project = None - - project_form = ProjectForm(post_data, instance=project) - if project_form.is_valid(): - project = project_form.save(commit=False) - project.owner = user - project.save() - for c in post_data.getlist('classifiers'): - classifier, created = Classifier.objects.get_or_create(name=c) - project.classifiers.add(classifier) - if files: - allow_overwrite = getattr(settings, - "DJANGOPYPI_ALLOW_VERSION_OVERWRITE", False) - try: - release = Release.objects.get(version=post_data['version'], - project=project, - distribution=UPLOAD_TO + '/' + - files['distribution']._name) - if not allow_overwrite: - return HttpResponseForbidden(ALREADY_EXISTS_FMT % ( - release.filename, release)) - except Release.DoesNotExist: - release = None - - # If the old file already exists, django will append a _ after the - # filename, however with .tar.gz files django does the "wrong" - # thing and saves it as project-0.1.2.tar_.gz. So remove it before - # django sees anything. - release_form = ReleaseForm(post_data, files, instance=release) - if release_form.is_valid(): - if release and os.path.exists(release.distribution.path): - os.remove(release.distribution.path) - release = release_form.save(commit=False) - release.project = project - release.save() - else: - return HttpResponseBadRequest( - "ERRORS: %s" % release_form.errors) - else: - return HttpResponseBadRequest("ERRORS: %s" % project_form.errors) - - return HttpResponse() - - -def register_or_upload(request, post_data, files): - user = login_basic_auth(request) - if not user: - return HttpResponseUnauthorized('pypi') - - login(request, user) - if not request.user.is_authenticated(): - return HttpResponseForbidden( - "Not logged in, or invalid username/password.") - - return submit_project_or_release(user, post_data, files) diff --git a/djangopypi/views/search.py b/djangopypi/views/search.py deleted file mode 100644 index 5d6a76b..0000000 --- a/djangopypi/views/search.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.template import RequestContext -from django.shortcuts import render_to_response -from django.db.models.query import Q - -from djangopypi.models import Project - - -def _search_query(q): - return Q(name__contains=q) | Q(summary__contains=q) - - -def search(request, template="djangopypi/search_results.html"): - context = RequestContext(request, {"dists": None, "search_term": ""}) - - if request.method == "POST": - search_term = context["search_term"] = request.POST.get("search_term") - if search_term: - query = _search_query(search_term) - context["dists"] = Project.objects.filter(query) - - if context["dists"] is None: - context["dists"] = Project.objects.all() - - return render_to_response(template, context_instance=context) diff --git a/djangopypi/views/users.py b/djangopypi/views/users.py deleted file mode 100644 index a58ac3e..0000000 --- a/djangopypi/views/users.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.http import HttpResponse, HttpResponseBadRequest - -from registration.forms import RegistrationForm -from registration.backends import get_backend - -DEFAULT_BACKEND = "registration.backends.default.DefaultBackend" - - -def create_user(request, post_data, files, backend_name=DEFAULT_BACKEND): - """Create new user from a distutil client request""" - form = RegistrationForm({"username": post_data["name"], - "email": post_data["email"], - "password1": post_data["password"], - "password2": post_data["password"]}) - if not form.is_valid(): - # Dist Utils requires error msg in HTTP status: "HTTP/1.1 400 msg" - # Which is HTTP/WSGI incompatible, so we're just returning a empty 400. - return HttpResponseBadRequest() - - backend = get_backend(backend_name) - if not backend.registration_allowed(request): - return HttpResponseBadRequest() - new_user = backend.register(request, **form.cleaned_data) - return HttpResponse("OK\n", status=200, mimetype='text/plain') diff --git a/index.html b/index.html deleted file mode 100644 index 29cef02..0000000 --- a/index.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - ask/chishop @ GitHub - - - - - - - Fork me on GitHub - -
- -
- - - - -
- -

chishop - by ask

- -
- Simple PyPI server written in Django. -
- -

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

-

Django >= 1.0.2

-

Install

-

see README.

-

License

-

BSD

-

Authors

-

Ask Solem (askh@modwheel.net)

-

Contact

-

Ask Solem Hoel (ask@modwheel.net)

- - -

Download

-

- You can download this project in either - zip or - tar formats. -

-

You can also clone the project with Git - by running: -

$ git clone git://github.com/ask/chishop
-

- - - -
- - - - diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4a68384 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +setuptools +docutils diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..306ec45 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_rpm] +doc_files = AUTHORS.rst Changelog.rst LICENSE README.rst diff --git a/setup.py b/setup.py index c83c08c..a08b1bb 100644 --- a/setup.py +++ b/setup.py @@ -1,88 +1,43 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- import os -import codecs +from setuptools import setup, find_packages +import userpypi -try: - from setuptools import setup, find_packages -except ImportError: - from ez_setup import use_setuptools - use_setuptools() - from setuptools import setup, find_packages - -from distutils.command.install_data import install_data -from distutils.command.install import INSTALL_SCHEMES -import sys - -djangopypi = __import__('djangopypi', {}, {}, ['']) - -packages, data_files = [], [] -root_dir = os.path.dirname(__file__) -if root_dir != '': - os.chdir(root_dir) -djangopypi_dir = "djangopypi" - -def osx_install_data(install_data): - def finalize_options(self): - self.set_undefined_options("install", ("install_lib", "install_dir")) - install_data.finalize_options(self) - -#if sys.platform == "darwin": -# cmdclasses = {'install_data': osx_install_data} -#else: -# cmdclasses = {'install_data': install_data} - - -def fullsplit(path, result=None): - if result is None: - result = [] - head, tail = os.path.split(path) - if head == '': - return [tail] + result - if head == path: - return result - return fullsplit(head, [tail] + result) - - -for scheme in INSTALL_SCHEMES.values(): - scheme['data'] = scheme['purelib'] +def fread(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() +try: + reqs = open(os.path.join(os.path.dirname(__file__), 'requirements.txt')).read() +except (IOError, OSError): + reqs = '' -for dirpath, dirnames, filenames in os.walk(djangopypi_dir): - # Ignore dirnames that start with '.' - for i, dirname in enumerate(dirnames): - if dirname.startswith("."): del dirnames[i] - for filename in filenames: - if filename.endswith(".py"): - packages.append('.'.join(fullsplit(dirpath))) - else: - data_files.append([dirpath, [os.path.join(dirpath, f) for f in - filenames]]) setup( - name='chishop', - version=djangopypi.__version__, - description='Simple PyPI server written in Django.', - author='Ask Solem', - author_email='askh@opera.com', - packages=packages, - url="http://ask.github.com/chishop", - zip_safe=False, - data_files=data_files, - install_requires=[ - 'django>=1.0', - 'docutils', - 'django-registration>0.7', - ], + name='userpypi', + version=userpypi.get_version(), + description="A Django application that emulates the Python Package Index.", + long_description=fread("README.rst")+"\n\n"+fread('Changelog.rst')+"\n\n"+fread('AUTHORS.rst'), classifiers=[ - "Development Status :: 3 - Alpha", - "Environment :: Web Environment", "Framework :: Django", + "Development Status :: 4 - Beta", + #"Development Status :: 5 - Production/Stable", + "Programming Language :: Python", + "Environment :: Web Environment", "Operating System :: OS Independent", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Topic :: System :: Software Distribution", "Programming Language :: Python", + "Topic :: Software Development :: Libraries :: Python Modules", ], - long_description=codecs.open('README', "r", "utf-8").read(), + keywords='django pypi packaging index', + author='Ask Solem', + author_email='askh@opera.com', + maintainer='Benjamin Liles', + maintainer_email='benliles@gmail.com', + url='http://github.com/benliles/chishop', + license='BSD', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=reqs, ) diff --git a/userpypi/__init__.py b/userpypi/__init__.py new file mode 100644 index 0000000..c3e8bea --- /dev/null +++ b/userpypi/__init__.py @@ -0,0 +1,27 @@ +""" +UserPyPI: A Python package index for individual users. +""" +__version_info__ = { + 'major': 0, + 'minor': 5, + 'micro': 7, + 'releaselevel': 'final', + 'serial': 1 +} + +def get_version(short=False): + assert __version_info__['releaselevel'] in ('alpha', 'beta', 'final') + vers = ["%(major)i.%(minor)i" % __version_info__, ] + if __version_info__['micro']: + vers.append(".%(micro)i" % __version_info__) + if __version_info__['releaselevel'] != 'final' and not short: + vers.append('%s%i' % (__version_info__['releaselevel'][0], __version_info__['serial'])) + return ''.join(vers) + +__version__ = get_version() + +try: + from userpypi import settings + from userpypi import signals +except ImportError: + pass diff --git a/userpypi/admin.py b/userpypi/admin.py new file mode 100644 index 0000000..d6e8d22 --- /dev/null +++ b/userpypi/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin + +from userpypi.models import * +from userpypi.settings import MIRRORING + +class PackageAdmin(admin.ModelAdmin): + list_display = ('name', 'owner',) + search_fields = ('name',) + +admin.site.register(Package, PackageAdmin) +admin.site.register(Release) +admin.site.register(Classifier) +admin.site.register(Distribution) + +if MIRRORING: + admin.site.register(MasterIndex) + admin.site.register(MirrorLog) diff --git a/userpypi/decorators.py b/userpypi/decorators.py new file mode 100644 index 0000000..1df3cf4 --- /dev/null +++ b/userpypi/decorators.py @@ -0,0 +1,88 @@ +from django.contrib.auth import login, REDIRECT_FIELD_NAME +from django.http import HttpResponseRedirect, HttpResponseForbidden +from django.utils.http import urlquote + +try: + from functools import wraps, WRAPPER_ASSIGNMENTS +except ImportError: + from django.utils.functional import wraps, WRAPPER_ASSIGNMENTS + +try: + from django.utils.decorators import available_attrs +except ImportError: + def available_attrs(fn): + return tuple(a for a in WRAPPER_ASSIGNMENTS if hasattr(fn, a)) + +from userpypi.http import HttpResponseUnauthorized, login_basic_auth + +# Find us a csrf exempt decorator that'll work with Django 1.0+ +try: + from django.views.decorators.csrf import csrf_exempt +except ImportError: + try: + from django.contrib.csrf.middleware import csrf_exempt + except ImportError: + def csrf_except(view_func): return view_func + +def basic_auth(view_func): + """ Decorator for views that need to handle basic authentication such as + distutils views. """ + + def _wrapped_view(request, *args, **kwargs): + if request.user.is_authenticated(): + return view_func(request, *args, **kwargs) + user = login_basic_auth(request) + + if not user: + return HttpResponseUnauthorized('pypi') + + login(request, user) + if not request.user.is_authenticated(): + return HttpResponseForbidden("Not logged in, or invalid username/" + "password.") + return view_func(request, *args, **kwargs) + return wraps(view_func, assigned=available_attrs(view_func))(_wrapped_view) + +def user_owns_package(login_url=None, redirect_field_name=REDIRECT_FIELD_NAME): + """ + Decorator for views that checks whether the user owns the currently requested + package. + """ + if not login_url: + from django.conf import settings + login_url = settings.LOGIN_URL + + def decorator(view_func): + def _wrapped_view(request, owner, package, *args, **kwargs): + if request.user.username != owner: + return HttpResponseForbidden() + if request.user.packages_owned.filter(name=package).count() > 0: + return view_func(request, owner=owner, package=package, *args, **kwargs) + + path = urlquote(request.get_full_path()) + tup = login_url, redirect_field_name, path + return HttpResponseRedirect('%s?%s=%s' % tup) + return wraps(view_func, assigned=available_attrs(view_func))(_wrapped_view) + return decorator + +def user_maintains_package(login_url=None, redirect_field_name=REDIRECT_FIELD_NAME): + """ + Decorator for views that checks whether the user maintains (or owns) the + currently requested package. + """ + if not login_url: + from django.conf import settings + login_url = settings.LOGIN_URL + + def decorator(view_func): + def _wrapped_view(request, owner, package, *args, **kwargs): + if (request.user.is_authenticated() and + (request.user.packages_owned.filter(name=package).count() > 0 or + request.user.packages_maintained.filter(name=package).count() > 0)): + return view_func(request, owner, package=package, *args, **kwargs) + + path = urlquote(request.get_full_path()) + tup = login_url, redirect_field_name, path + return HttpResponseRedirect('%s?%s=%s' % tup) + return wraps(view_func, assigned=available_attrs(view_func))(_wrapped_view) + return decorator diff --git a/userpypi/feeds.py b/userpypi/feeds.py new file mode 100644 index 0000000..646f7d6 --- /dev/null +++ b/userpypi/feeds.py @@ -0,0 +1,57 @@ +from django.shortcuts import get_object_or_404 +try: + from django.contrib.syndication.views import Feed, FeedDoesNotExist +except ImportError: + from django.contrib.syndication.feeds import Feed as BaseFeed, FeedDoesNotExist + from django.http import HttpResponse, Http404 + from django.core.exceptions import ObjectDoesNotExist + + class Feed(BaseFeed): + def __call__(self, request, *args, **kwargs): + try: + obj = self.get_object(request, *args, **kwargs) + except ObjectDoesNotExist: + raise Http404('Feed object does not exist.') + feedgen = self.get_feed(obj, request) + response = HttpResponse(mimetype=feedgen.mime_type) + feedgen.write(response, 'utf-8') + return response + +from userpypi.models import Package, Release + + + +class ReleaseFeed(Feed): + """ A feed of releases either for the site in general or for a specific + package. """ + + def get_object(self, request, package=None, **kwargs): + if package: + return get_object_or_404(Package, name=package) + return request.build_absolute_uri('/') + + def link(self, obj): + if isinstance(obj, Package): + return obj.get_absolute_url() + return obj + + def title(self, obj): + if isinstance(obj, Package): + return u'Releases for %s' % (obj.name,) + return u'Package index releases' + + def description(self, obj): + if isinstance(obj, Package): + return u'Recent releases for the package: %s' % (obj.name,) + return u'Recent releases on the package index server' + + def items(self, obj): + if isinstance(obj, Package): + return obj.releases.filter(hidden=False).order_by('-created')[:25] + return Release.objects.filter(hidden=False).order_by('-created')[:40] + + def item_description(self, item): + if isinstance(item, Release): + if item.summary: + return item.summary + return super(ReleaseFeed, self).item_description(item) \ No newline at end of file diff --git a/userpypi/forms.py b/userpypi/forms.py new file mode 100644 index 0000000..9df493c --- /dev/null +++ b/userpypi/forms.py @@ -0,0 +1,277 @@ +from os.path import basename + +from django import forms +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ +from django.forms.models import inlineformset_factory + +from userpypi.settings import ALLOW_VERSION_OVERWRITE, METADATA_FIELDS +from userpypi.models import (Package, Classifier, Release, Distribution, + Maintainer) + +from django.contrib.auth.models import User +from selectable.base import ModelLookup +from selectable.forms import AutoCompleteSelectField +from selectable.registry import registry + +class UserLookup(ModelLookup): + model = User + search_fields = ( + 'username__icontains', + 'first_name__icontains', + 'last_name__icontains', + ) + filters = {'is_active': True, } + + def get_item_value(self, item): + # Display for currently selected item + return item.username + + def get_item_label(self, item): + # Display for choice listings + return u"%s (%s)" % (item.username, item.get_full_name()) +registry.register(UserLookup) + +class SimplePackageSearchForm(forms.Form): + query = forms.CharField(max_length=255) + +class MaintainerForm(forms.ModelForm): + user = AutoCompleteSelectField(lookup_class=UserLookup) + + class Meta: + model = Maintainer +MaintainerFormSet = inlineformset_factory(Package, Maintainer, + form=MaintainerForm, extra=1) + +class PackageForm(forms.ModelForm): + class Meta: + model = Package + exclude = ['maintainers'] + widgets = { + 'owner': forms.HiddenInput, + 'name': forms.HiddenInput, + 'private': forms.HiddenInput + } + +class DistributionUploadForm(forms.ModelForm): + class Meta: + model = Distribution + fields = ('content', 'comment', 'filetype', 'pyversion',) + + def clean_content(self): + content = self.cleaned_data['content'] + storage = self.instance.content.storage + field = self.instance.content.field + name = field.generate_filename(instance=self.instance, + filename=content.name) + + if not storage.exists(name): + print '%s does not exist' % (name,) + return content + + if ALLOW_VERSION_OVERWRITE: + raise forms.ValidationError('Version overwrite is not yet handled') + + raise forms.ValidationError('That distribution already exists, please ' + 'delete it first before uploading a new ' + 'version.') + + +class ReleaseForm(forms.ModelForm): + metadata_version = forms.CharField( + widget=forms.Select(choices=zip(METADATA_FIELDS.keys(), + METADATA_FIELDS.keys()))) + + class Meta: + model = Release + exclude = ['package', 'version', 'package_info'] + +metadata10licenses = ('Artistic', 'BSD', 'DFSG', 'GNU GPL', 'GNU LGPL', + 'MIT', 'Mozilla PL', 'public domain', 'Python', + 'Qt', 'PL', 'Zope PL', 'unknown', 'nocommercial', 'nosell', + 'nosource', 'shareware', 'other') + +class LinesField(forms.CharField): + def __init__(self, *args, **kwargs): + kwargs.setdefault('widget', forms.Textarea()) + super(LinesField, self).__init__(*args, **kwargs) + + def to_python(self, value): + return map(lambda s: s.strip(), + super(LinesField, self).to_python(value).split('\n')) + +class Metadata10Form(forms.Form): + platform = LinesField(required=False, + help_text=_(u'A comma-separated list of platform ' + 'specifications, summarizing the ' + 'operating systems supported by the ' + 'package.')) + + summary = forms.CharField(help_text=_(u'A one-line summary of what the ' + 'package does.')) + + description = forms.CharField(required=False, + widget=forms.Textarea(attrs=dict(rows=40, + columns=40)), + help_text=_(u'A longer description of the ' + 'package that can run to several ' + 'paragraphs. If this is in ' + 'reStructuredText format, it will ' + 'be rendered nicely on display.')) + + keywords = forms.CharField(help_text=_(u'A list of additional keywords to ' + 'be used to assist searching for the ' + 'package in a larger catalog')) + + home_page = forms.URLField(required=False, verify_exists=True, + help_text=_(u'A string containing the URL for ' + 'the package\'s home page.')) + + author = forms.CharField(required=False, + widget=forms.Textarea(attrs=dict(rows=3, + columns=20)), + help_text=_(u'A string containing at a minimum the ' + 'author\'s name. Contact information ' + 'can also be added, separating each ' + 'line with newlines.')) + + author_email = forms.CharField(help_text=_(u'A string containing the ' + 'author\'s e-mail address. It ' + 'can contain a name and e-mail ' + 'address in the legal forms for ' + 'a RFC-822 \'From:\' header.')) + + license = forms.CharField(max_length=32, + help_text=_(u'A string selected from a short list ' + 'of choices, specifying the license ' + 'covering the package.'), + widget=forms.Select(choices=(zip(metadata10licenses, + metadata10licenses)))) + +class Metadata11Form(Metadata10Form): + supported_platform = forms.CharField(required=False, widget=forms.Textarea(), + help_text=_(u'The OS and CPU for which ' + 'the binary package was ' + 'compiled.')) + + keywords = forms.CharField(required=False, + help_text=_(u'A list of additional keywords to ' + 'be used to assist searching for the ' + 'package in a larger catalog')) + + download_url = forms.URLField(required=False, verify_exists=True, + help_text=_(u'A string containing the URL for ' + 'the package\'s home page.')) + + license = forms.CharField(required=False, widget=forms.Textarea(), + help_text=_(u'Text indicating the license ' + 'covering the package where the ' + 'license is not a selection from the ' + '"License" Trove classifiers.')) + + classifier = forms.ModelMultipleChoiceField(required=False, + queryset=Classifier.objects.all(), + help_text=_(u'Trove classifiers')) + + requires = LinesField(required=False, + help_text=_(u'Each line contains a string describing ' + 'some other module or package required by ' + 'this package.')) + + provides = LinesField(required=False, + help_text=_(u'Each line contains a string describing ' + 'a package or module that will be ' + 'provided by this package once it is ' + 'installed')) + + obsoletes = LinesField(required=False, + help_text=_(u'Each line contains a string describing ' + 'a package or module that this package ' + 'renders obsolete, meaning that the two ' + 'packages should not be installed at the ' + 'same time')) + +class Metadata12Form(Metadata10Form): + supported_platform = forms.CharField(required=False, widget=forms.Textarea(), + help_text=_(u'The OS and CPU for which ' + 'the binary package was ' + 'compiled.')) + + keywords = forms.CharField(required=False, + help_text=_(u'A list of additional keywords to ' + 'be used to assist searching for the ' + 'package in a larger catalog')) + + download_url = forms.URLField(required=False, + verify_exists=True, + help_text=_(u'A string containing the URL for ' + 'the package\'s home page.')) + + author_email = forms.CharField(required=False, + help_text=_(u'A string containing the ' + 'author\'s e-mail address. It ' + 'can contain a name and e-mail ' + 'address in the legal forms for ' + 'a RFC-822 \'From:\' header.')) + + maintainer = forms.CharField(required=False, widget=forms.Textarea(), + help_text=_(u'A string containing at a minimum ' + 'the maintainer\'s name. Contact ' + 'information can also be added, ' + 'separating each line with ' + 'newlines.')) + maintainer_email = forms.CharField(required=False, + help_text=_(u'A string containing the ' + 'maintainer\'s e-mail address. ' + 'It can contain a name and ' + 'e-mail address in the legal ' + 'forms for a RFC-822 ' + '\'From:\' header.')) + + license = forms.CharField(required=False, widget=forms.Textarea(), + help_text=_(u'Text indicating the license ' + 'covering the package where the ' + 'license is not a selection from the ' + '"License" Trove classifiers.')) + + classifier = forms.ModelMultipleChoiceField(required=False, + queryset=Classifier.objects.all(), + help_text=_(u'Trove classifiers')) + + requires_dist = LinesField(required=False, + help_text=_(u'Each line contains a string ' + 'describing some other module or ' + 'package required by this package.')) + + provides_dist = LinesField(required=False, + help_text=_(u'Each line contains a string ' + 'describing a package or module that ' + 'will be provided by this package ' + 'once it is installed')) + + obsoletes_dist = LinesField(required=False, + help_text=_(u'Each line contains a string ' + 'describing a package or module that ' + 'this package renders obsolete, ' + 'meaning that the two packages ' + 'should not be installed at the ' + 'same time')) + + requires_python = forms.CharField(required=False, + help_text=_(u'This field specifies the ' + 'Python version(s) that the ' + 'distribution is guaranteed ' + 'to be compatible with.')) + + requires_external = forms.CharField(required=False, widget=forms.Textarea(), + help_text=_(u'Each line contains a ' + 'string describing some ' + 'dependency in the system ' + 'that the distribution is ' + 'to be used.')) + project_url = forms.CharField(required=False, widget=forms.Textarea(), + help_text=_(u'Each line is a string containing ' + 'a browsable URL for the project ' + 'and a label for it, separated ' + 'by a comma: "Bug Tracker, ' + 'http://bugs.project.com"')) diff --git a/userpypi/http.py b/userpypi/http.py new file mode 100644 index 0000000..3d88260 --- /dev/null +++ b/userpypi/http.py @@ -0,0 +1,92 @@ +from django.http import HttpResponse, QueryDict +from django.core.files.uploadedfile import TemporaryUploadedFile +from django.utils.datastructures import MultiValueDict +from django.contrib.auth import authenticate + + +class HttpResponseNotImplemented(HttpResponse): + status_code = 501 + + +class HttpResponseUnauthorized(HttpResponse): + status_code = 401 + + def __init__(self, realm): + HttpResponse.__init__(self) + self['WWW-Authenticate'] = 'Basic realm="%s"' % realm + + +def parse_distutils_request(request): + """ This is being used because the built in request parser that Django uses, + django.http.multipartparser.MultiPartParser is interperting the POST data + incorrectly and/or the post data coming from distutils is invalid. + + One portion of this is the end marker: \r\n\r\n (what Django expects) + versus \n\n (what distutils is sending). + """ + + try: + sep = request.raw_post_data.splitlines()[1] + except: + raise ValueError('Invalid post data') + + + request.POST = QueryDict('',mutable=True) + try: + request._files = MultiValueDict() + except Exception, e: + pass + + for part in filter(lambda e: e.strip(), request.raw_post_data.split(sep)): + try: + header, content = part.lstrip().split('\n',1) + except Exception, e: + continue + + if content.startswith('\n'): + content = content[1:] + + if content.endswith('\n'): + content = content[:-1] + + headers = parse_header(header) + + if "name" not in headers: + continue + + if "filename" in headers: + dist = TemporaryUploadedFile(name=headers["filename"], + size=len(content), + content_type="application/gzip", + charset='utf-8') + dist.write(content) + dist.seek(0) + request.FILES.appendlist(headers['name'], dist) + else: + request.POST.appendlist(headers["name"],content) + return + +def parse_header(header): + headers = {} + for kvpair in filter(lambda p: p, + map(lambda p: p.strip(), + header.split(';'))): + try: + key, value = kvpair.split("=",1) + except ValueError: + continue + headers[key.strip()] = value.strip('"') + + return headers + + +def login_basic_auth(request): + authentication = request.META.get("HTTP_AUTHORIZATION") + if not authentication: + return + (authmeth, auth) = authentication.split(' ', 1) + if authmeth.lower() != "basic": + return + auth = auth.strip().decode("base64") + username, password = auth.split(":", 1) + return authenticate(username=username, password=password) diff --git a/chishop/conf/__init__.py b/userpypi/management/__init__.py similarity index 100% rename from chishop/conf/__init__.py rename to userpypi/management/__init__.py diff --git a/chishop/media/__init__.py b/userpypi/management/commands/__init__.py similarity index 100% rename from chishop/media/__init__.py rename to userpypi/management/commands/__init__.py diff --git a/djangopypi/management/commands/loadclassifiers.py b/userpypi/management/commands/loadclassifiers.py similarity index 94% rename from djangopypi/management/commands/loadclassifiers.py rename to userpypi/management/commands/loadclassifiers.py index 49e2642..1fc9c71 100644 --- a/djangopypi/management/commands/loadclassifiers.py +++ b/userpypi/management/commands/loadclassifiers.py @@ -3,7 +3,7 @@ pypi, or from a file/url. Note, pypi docs says to not add classifiers that are not used in submitted -projects. On the other hand it can be usefull to have a list of classifiers +packages. On the other hand it can be usefull to have a list of classifiers to choose if you have to modify package data. Use it if you need it. """ @@ -12,7 +12,7 @@ import os.path from django.core.management.base import BaseCommand -from djangopypi.models import Classifier +from userpypi.models import Classifier CLASSIFIERS_URL = "http://pypi.python.org/pypi?%3Aaction=list_classifiers" diff --git a/djangopypi/management/commands/ppadd.py b/userpypi/management/commands/ppadd.py similarity index 81% rename from djangopypi/management/commands/ppadd.py rename to userpypi/management/commands/ppadd.py index aa45274..3d78d7c 100644 --- a/djangopypi/management/commands/ppadd.py +++ b/userpypi/management/commands/ppadd.py @@ -18,7 +18,7 @@ from urlparse import urlsplit from setuptools.package_index import PackageIndex from django.contrib.auth.models import User -from djangopypi.models import Project, Release, Classifier +from userpypi.models import Package, Release, Classifier @@ -28,7 +28,7 @@ def tempdir(): """Simple context that provides a temporary directory that is deleted when the context is exited.""" - d = tempfile.mkdtemp(".tmp", "djangopypi.") + d = tempfile.mkdtemp(".tmp", "userpypi.") yield d shutil.rmtree(d) @@ -68,14 +68,14 @@ def _save_package(self, path, ownerid): try: # can't use get_or_create as that demands there be an owner - project = Project.objects.get(name=meta.name) - isnewproject = False - except Project.DoesNotExist: - project = Project(name=meta.name) - isnewproject = True - - release = project.get_release(meta.version) - if not isnewproject and release and release.version == meta.version: + package = Package.objects.get(name=meta.name) + isnewpackage = False + except Package.DoesNotExist: + package = Package(name=meta.name) + isnewpackage = True + + release = package.get_release(meta.version) + if not isnewpackage and release and release.version == meta.version: print "%s-%s already added" % (meta.name, meta.version) return @@ -105,27 +105,27 @@ def _save_package(self, path, ownerid): # at this point we have metadata and an owner, can safely add it. - project.owner = owner + package.owner = owner # Some packages don't have proper licence, seems to be a problem # with setup.py upload. Use "UNKNOWN" - project.license = meta.license or "Unknown" - project.metadata_version = meta.metadata_version - project.author = meta.author - project.home_page = meta.home_page - project.download_url = meta.download_url - project.summary = meta.summary - project.description = meta.description - project.author_email = meta.author_email + package.license = meta.license or "Unknown" + package.metadata_version = meta.metadata_version + package.author = meta.author + package.home_page = meta.home_page + package.download_url = meta.download_url + package.summary = meta.summary + package.description = meta.description + package.author_email = meta.author_email - project.save() + package.save() for classifier in meta.classifiers: - project.classifiers.add( + package.classifiers.add( Classifier.objects.get_or_create(name=classifier)[0]) release = Release() release.version = meta.version - release.project = project + release.package = package filename = os.path.basename(path) file = File(open(path, "rb")) diff --git a/userpypi/management/commands/update_mirrors.py b/userpypi/management/commands/update_mirrors.py new file mode 100644 index 0000000..b3cdd9a --- /dev/null +++ b/userpypi/management/commands/update_mirrors.py @@ -0,0 +1,82 @@ +""" + +""" + +from datetime import datetime +import urllib +import os.path +from time import mktime +from xmlrpclib import ServerProxy + +from django.core.files import File +from django.core.management.base import BaseCommand +from userpypi.models import * + + + +class Command(BaseCommand): + help = """Load all classifiers from pypi. If any arguments are given, +they will be used as paths or urls for classifiers instead of using the +official pypi list url""" + + def handle(self, *args, **options): + for index in MasterIndex.objects.all(): + rpc = ServerProxy(index.url) + try: + last = index.logs.latest() + except: + print 'Error getting latest update for %s' % (str(index),) + continue + + newer = MirrorLog.objects.create(master=index, created=datetime.now()) + + print 'Looking at changes since: %d' % (mktime(last.created.timetuple()),) + for update in rpc.changelog(int(mktime(last.created.timetuple()))): + print str(update) + package, created = Package.objects.get_or_create(name=update[0]) + + if update[3] == 'new release': + release, created = Release.objects.get_or_create( + package=package, version=update[1]) + + if created: + newer.releases_added.add(release) + + pkg_data = rpc.release_data(update[0],update[1]) + print 'Retrieved release data: %s' % (str(pkg_data),) + if 'name' in pkg_data: + del pkg_data['name'] + if 'version' in pkg_data: + del pkg_data['version'] + + for key, value in pkg_data.iteritems(): + if key != 'classifier': + release.package_info[key] = value + else: + release.package_info.setlist(key, value) + + release.save() + elif update[3] == 'remove': + Release.objects.filter(package=package, version=update[1]).delete() + elif update[3].startswith('add source file'): + try: + release = Release.objects.get(package=package, + version=update[1]) + except Release.DoesNotExist: + continue + downloads = rpc.release_urls(update[0], update[1]) + for download in downloads: + print 'Download data: %s' % (str(download),) + dist, created = Distribution.objects.get_or_create(release=release, + filetype=download['packagetype'], + pyversion=download['python_version']) + + if not created and dist.md5_digest != download['md5_digest']: + dist.md5_digest = download['md5_digest'] + dist.content = File(urllib.urlopen(download['url'])) + + dist.comment = download['comment_text'] + + dist.save() + + \ No newline at end of file diff --git a/userpypi/migrations/0001_initial.py b/userpypi/migrations/0001_initial.py new file mode 100644 index 0000000..bbfeeda --- /dev/null +++ b/userpypi/migrations/0001_initial.py @@ -0,0 +1,153 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'Classifier' + db.create_table('userpypi_classifier', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), + )) + db.send_create_signal('userpypi', ['Classifier']) + + # Adding model 'Project' + db.create_table('userpypi_project', ( + ('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + ('license', self.gf('django.db.models.fields.TextField')(blank=True)), + ('metadata_version', self.gf('django.db.models.fields.CharField')(default=1.0, max_length=64)), + ('author', self.gf('django.db.models.fields.CharField')(max_length=128, blank=True)), + ('home_page', self.gf('django.db.models.fields.URLField')(max_length=200, null=True, blank=True)), + ('description', self.gf('django.db.models.fields.TextField')(blank=True)), + ('download_url', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + ('summary', self.gf('django.db.models.fields.TextField')(blank=True)), + ('author_email', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('owner', self.gf('django.db.models.fields.related.ForeignKey')(related_name='projects', to=orm['auth.User'])), + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), + )) + db.send_create_signal('userpypi', ['Project']) + + # Adding M2M table for field classifiers on 'Project' + db.create_table('userpypi_project_classifiers', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('project', models.ForeignKey(orm['userpypi.project'], null=False)), + ('classifier', models.ForeignKey(orm['userpypi.classifier'], null=False)) + )) + db.create_unique('userpypi_project_classifiers', ['project_id', 'classifier_id']) + + # Adding model 'Release' + db.create_table('userpypi_release', ( + ('upload_time', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + ('md5_digest', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('filetype', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('pyversion', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('project', self.gf('django.db.models.fields.related.ForeignKey')(related_name='releases', to=orm['userpypi.Project'])), + ('platform', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('version', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('signature', self.gf('django.db.models.fields.CharField')(max_length=128, blank=True)), + ('distribution', self.gf('django.db.models.fields.files.FileField')(max_length=100)), + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + )) + db.send_create_signal('userpypi', ['Release']) + + # Adding unique constraint on 'Release', fields ['project', 'version', 'platform', 'distribution', 'pyversion'] + db.create_unique('userpypi_release', ['project_id', 'version', 'platform', 'distribution', 'pyversion']) + + + def backwards(self, orm): + + # Deleting model 'Classifier' + db.delete_table('userpypi_classifier') + + # Deleting model 'Project' + db.delete_table('userpypi_project') + + # Removing M2M table for field classifiers on 'Project' + db.delete_table('userpypi_project_classifiers') + + # Deleting model 'Release' + db.delete_table('userpypi_release') + + # Removing unique constraint on 'Release', fields ['project', 'version', 'platform', 'distribution', 'pyversion'] + db.delete_unique('userpypi_release', ['project_id', 'version', 'platform', 'distribution', 'pyversion']) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'userpypi.classifier': { + 'Meta': {'object_name': 'Classifier'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'userpypi.project': { + 'Meta': {'object_name': 'Project'}, + 'author': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'author_email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'classifiers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['userpypi.Classifier']"}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'download_url': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'home_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'license': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'metadata_version': ('django.db.models.fields.CharField', [], {'default': '1.0', 'max_length': '64'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'to': "orm['auth.User']"}), + 'summary': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'userpypi.release': { + 'Meta': {'unique_together': "(('project', 'version', 'platform', 'distribution', 'pyversion'),)", 'object_name': 'Release'}, + 'distribution': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'filetype': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'md5_digest': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'platform': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'releases'", 'to': "orm['userpypi.Project']"}), + 'pyversion': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'signature': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'upload_time': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + } + } + + complete_apps = ['userpypi'] diff --git a/userpypi/migrations/0002_refactoring.py b/userpypi/migrations/0002_refactoring.py new file mode 100644 index 0000000..524cfd0 --- /dev/null +++ b/userpypi/migrations/0002_refactoring.py @@ -0,0 +1,292 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Deleting model 'Project' + db.delete_table('userpypi_project') + + # Removing M2M table for field classifiers on 'Project' + db.delete_table('userpypi_project_classifiers') + + # Adding model 'Distribution' + db.create_table('userpypi_distribution', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('release', self.gf('django.db.models.fields.related.ForeignKey')(related_name='distributions', to=orm['userpypi.Release'])), + ('content', self.gf('django.db.models.fields.files.FileField')(max_length=100)), + ('md5_digest', self.gf('django.db.models.fields.CharField')(max_length=32, blank=True)), + ('filetype', self.gf('django.db.models.fields.CharField')(max_length=32)), + ('pyversion', self.gf('django.db.models.fields.CharField')(max_length=16, blank=True)), + ('comment', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('signature', self.gf('django.db.models.fields.TextField')(blank=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('uploader', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + )) + db.send_create_signal('userpypi', ['Distribution']) + + # Adding model 'Review' + db.create_table('userpypi_review', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('release', self.gf('django.db.models.fields.related.ForeignKey')(related_name='reviews', to=orm['userpypi.Release'])), + ('rating', self.gf('django.db.models.fields.PositiveSmallIntegerField')(blank=True)), + ('comment', self.gf('django.db.models.fields.TextField')(blank=True)), + )) + db.send_create_signal('userpypi', ['Review']) + + # Adding model 'Package' + db.create_table('userpypi_package', ( + ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255, primary_key=True)), + ('auto_hide', self.gf('django.db.models.fields.BooleanField')(default=True, blank=True)), + ('allow_comments', self.gf('django.db.models.fields.BooleanField')(default=True, blank=True)), + )) + db.send_create_signal('userpypi', ['Package']) + + # Adding M2M table for field owners on 'Package' + db.create_table('userpypi_package_owners', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('package', models.ForeignKey(orm['userpypi.package'], null=False)), + ('user', models.ForeignKey(orm['auth.user'], null=False)) + )) + db.create_unique('userpypi_package_owners', ['package_id', 'user_id']) + + # Adding M2M table for field maintainers on 'Package' + db.create_table('userpypi_package_maintainers', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('package', models.ForeignKey(orm['userpypi.package'], null=False)), + ('user', models.ForeignKey(orm['auth.user'], null=False)) + )) + db.create_unique('userpypi_package_maintainers', ['package_id', 'user_id']) + + # Deleting field 'Classifier.id' + db.delete_column('userpypi_classifier', 'id') + + # Changing field 'Classifier.name' + db.alter_column('userpypi_classifier', 'name', self.gf('django.db.models.fields.CharField')(max_length=255, primary_key=True)) + + # Deleting field 'Release.md5_digest' + db.delete_column('userpypi_release', 'md5_digest') + + # Deleting field 'Release.filetype' + db.delete_column('userpypi_release', 'filetype') + + # Deleting field 'Release.upload_time' + db.delete_column('userpypi_release', 'upload_time') + + # Deleting field 'Release.pyversion' + db.delete_column('userpypi_release', 'pyversion') + + # Deleting field 'Release.project' + db.delete_column('userpypi_release', 'project_id') + + # Deleting field 'Release.platform' + db.delete_column('userpypi_release', 'platform') + + # Deleting field 'Release.signature' + db.delete_column('userpypi_release', 'signature') + + # Deleting field 'Release.distribution' + db.delete_column('userpypi_release', 'distribution') + + # Adding field 'Release.package' + db.add_column('userpypi_release', 'package', self.gf('django.db.models.fields.related.ForeignKey')(default='', related_name='releases', to=orm['userpypi.Package']), keep_default=False) + + # Adding field 'Release.metadata_version' + db.add_column('userpypi_release', 'metadata_version', self.gf('django.db.models.fields.CharField')(default='1.0', max_length=64), keep_default=False) + + # Adding field 'Release.package_info' + db.add_column('userpypi_release', 'package_info', self.gf('userpypi.models.PackageInfoField')(default=''), keep_default=False) + + # Adding field 'Release.hidden' + db.add_column('userpypi_release', 'hidden', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True), keep_default=False) + + # Adding field 'Release.created' + db.add_column('userpypi_release', 'created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, default='', blank=True), keep_default=False) + + # Removing unique constraint on 'Release', fields ['project', 'platform', 'distribution', 'version', 'pyversion'] + db.delete_unique('userpypi_release', ['project_id', 'platform', 'distribution', 'version', 'pyversion']) + + # Adding unique constraint on 'Release', fields ['version', 'package'] + db.create_unique('userpypi_release', ['version', 'package_id']) + + + def backwards(self, orm): + + # Adding model 'Project' + db.create_table('userpypi_project', ( + ('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + ('description', self.gf('django.db.models.fields.TextField')(blank=True)), + ('metadata_version', self.gf('django.db.models.fields.CharField')(default=1.0, max_length=64)), + ('owner', self.gf('django.db.models.fields.related.ForeignKey')(related_name='projects', to=orm['auth.User'])), + ('summary', self.gf('django.db.models.fields.TextField')(blank=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255, unique=True)), + ('license', self.gf('django.db.models.fields.TextField')(blank=True)), + ('author', self.gf('django.db.models.fields.CharField')(max_length=128, blank=True)), + ('home_page', self.gf('django.db.models.fields.URLField')(max_length=200, null=True, blank=True)), + ('download_url', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)), + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('author_email', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + )) + db.send_create_signal('userpypi', ['Project']) + + # Adding M2M table for field classifiers on 'Project' + db.create_table('userpypi_project_classifiers', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('project', models.ForeignKey(orm['userpypi.project'], null=False)), + ('classifier', models.ForeignKey(orm['userpypi.classifier'], null=False)) + )) + db.create_unique('userpypi_project_classifiers', ['project_id', 'classifier_id']) + + # Deleting model 'Distribution' + db.delete_table('userpypi_distribution') + + # Deleting model 'Review' + db.delete_table('userpypi_review') + + # Deleting model 'Package' + db.delete_table('userpypi_package') + + # Removing M2M table for field owners on 'Package' + db.delete_table('userpypi_package_owners') + + # Removing M2M table for field maintainers on 'Package' + db.delete_table('userpypi_package_maintainers') + + # Adding field 'Classifier.id' + db.add_column('userpypi_classifier', 'id', self.gf('django.db.models.fields.AutoField')(default='', primary_key=True), keep_default=False) + + # Changing field 'Classifier.name' + db.alter_column('userpypi_classifier', 'name', self.gf('django.db.models.fields.CharField')(max_length=255, unique=True)) + + # Adding field 'Release.md5_digest' + db.add_column('userpypi_release', 'md5_digest', self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True), keep_default=False) + + # Adding field 'Release.filetype' + db.add_column('userpypi_release', 'filetype', self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True), keep_default=False) + + # Adding field 'Release.upload_time' + db.add_column('userpypi_release', 'upload_time', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, default='', blank=True), keep_default=False) + + # Adding field 'Release.pyversion' + db.add_column('userpypi_release', 'pyversion', self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True), keep_default=False) + + # Adding field 'Release.project' + db.add_column('userpypi_release', 'project', self.gf('django.db.models.fields.related.ForeignKey')(default='', related_name='releases', to=orm['userpypi.Project']), keep_default=False) + + # Adding field 'Release.platform' + db.add_column('userpypi_release', 'platform', self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True), keep_default=False) + + # Adding field 'Release.signature' + db.add_column('userpypi_release', 'signature', self.gf('django.db.models.fields.CharField')(default='', max_length=128, blank=True), keep_default=False) + + # Adding field 'Release.distribution' + db.add_column('userpypi_release', 'distribution', self.gf('django.db.models.fields.files.FileField')(default='', max_length=100), keep_default=False) + + # Deleting field 'Release.package' + db.delete_column('userpypi_release', 'package_id') + + # Deleting field 'Release.metadata_version' + db.delete_column('userpypi_release', 'metadata_version') + + # Deleting field 'Release.package_info' + db.delete_column('userpypi_release', 'package_info') + + # Deleting field 'Release.hidden' + db.delete_column('userpypi_release', 'hidden') + + # Deleting field 'Release.created' + db.delete_column('userpypi_release', 'created') + + # Adding unique constraint on 'Release', fields ['project', 'platform', 'distribution', 'version', 'pyversion'] + db.create_unique('userpypi_release', ['project_id', 'platform', 'distribution', 'version', 'pyversion']) + + # Removing unique constraint on 'Release', fields ['version', 'package'] + db.delete_unique('userpypi_release', ['version', 'package_id']) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'userpypi.classifier': { + 'Meta': {'object_name': 'Classifier'}, + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}) + }, + 'userpypi.distribution': { + 'Meta': {'unique_together': "(('release', 'filetype', 'pyversion'),)", 'object_name': 'Distribution'}, + 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'filetype': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'md5_digest': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'pyversion': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'distributions'", 'to': "orm['userpypi.Release']"}), + 'signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'uploader': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'userpypi.package': { + 'Meta': {'object_name': 'Package'}, + 'allow_comments': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'auto_hide': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'maintainers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'packages_maintained'", 'blank': 'True', 'to': "orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'primary_key': 'True'}), + 'owners': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'packages_owned'", 'blank': 'True', 'to': "orm['auth.User']"}) + }, + 'userpypi.release': { + 'Meta': {'unique_together': "(('package', 'version'),)", 'object_name': 'Release'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'metadata_version': ('django.db.models.fields.CharField', [], {'default': "'1.0'", 'max_length': '64'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'releases'", 'to': "orm['userpypi.Package']"}), + 'package_info': ('userpypi.models.PackageInfoField', [], {}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'userpypi.review': { + 'Meta': {'object_name': 'Review'}, + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'rating': ('django.db.models.fields.PositiveSmallIntegerField', [], {'blank': 'True'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reviews'", 'to': "orm['userpypi.Release']"}) + } + } + + complete_apps = ['userpypi'] diff --git a/userpypi/migrations/0003_add_masterindex_mirrorlog.py b/userpypi/migrations/0003_add_masterindex_mirrorlog.py new file mode 100644 index 0000000..aa5ccf3 --- /dev/null +++ b/userpypi/migrations/0003_add_masterindex_mirrorlog.py @@ -0,0 +1,142 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'MirrorLog' + db.create_table('userpypi_mirrorlog', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('master', self.gf('django.db.models.fields.related.ForeignKey')(related_name='logs', to=orm['userpypi.MasterIndex'])), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + )) + db.send_create_signal('userpypi', ['MirrorLog']) + + # Adding M2M table for field releases_added on 'MirrorLog' + db.create_table('userpypi_mirrorlog_releases_added', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('mirrorlog', models.ForeignKey(orm['userpypi.mirrorlog'], null=False)), + ('release', models.ForeignKey(orm['userpypi.release'], null=False)) + )) + db.create_unique('userpypi_mirrorlog_releases_added', ['mirrorlog_id', 'release_id']) + + # Adding model 'MasterIndex' + db.create_table('userpypi_masterindex', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('url', self.gf('django.db.models.fields.CharField')(max_length=255)), + )) + db.send_create_signal('userpypi', ['MasterIndex']) + + + def backwards(self, orm): + + # Deleting model 'MirrorLog' + db.delete_table('userpypi_mirrorlog') + + # Removing M2M table for field releases_added on 'MirrorLog' + db.delete_table('userpypi_mirrorlog_releases_added') + + # Deleting model 'MasterIndex' + db.delete_table('userpypi_masterindex') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'userpypi.classifier': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Classifier'}, + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}) + }, + 'userpypi.distribution': { + 'Meta': {'unique_together': "(('release', 'filetype', 'pyversion'),)", 'object_name': 'Distribution'}, + 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'filetype': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'md5_digest': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'pyversion': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'distributions'", 'to': "orm['userpypi.Release']"}), + 'signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'uploader': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'userpypi.masterindex': { + 'Meta': {'object_name': 'MasterIndex'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'userpypi.mirrorlog': { + 'Meta': {'object_name': 'MirrorLog'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'master': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'logs'", 'to': "orm['userpypi.MasterIndex']"}), + 'releases_added': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mirror_sources'", 'symmetrical': 'False', 'to': "orm['userpypi.Release']"}) + }, + 'userpypi.package': { + 'Meta': {'ordering': "['name']", 'object_name': 'Package'}, + 'allow_comments': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'auto_hide': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'maintainers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'packages_maintained'", 'blank': 'True', 'to': "orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'primary_key': 'True'}), + 'owners': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'packages_owned'", 'blank': 'True', 'to': "orm['auth.User']"}) + }, + 'userpypi.release': { + 'Meta': {'ordering': "['-created']", 'unique_together': "(('package', 'version'),)", 'object_name': 'Release'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'metadata_version': ('django.db.models.fields.CharField', [], {'default': "'1.0'", 'max_length': '64'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'releases'", 'to': "orm['userpypi.Package']"}), + 'package_info': ('userpypi.models.PackageInfoField', [], {}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'userpypi.review': { + 'Meta': {'object_name': 'Review'}, + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'rating': ('django.db.models.fields.PositiveSmallIntegerField', [], {'blank': 'True'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reviews'", 'to': "orm['userpypi.Release']"}) + } + } + + complete_apps = ['userpypi'] diff --git a/userpypi/migrations/0004_allow_anonymous_distributions.py b/userpypi/migrations/0004_allow_anonymous_distributions.py new file mode 100644 index 0000000..bb9fe49 --- /dev/null +++ b/userpypi/migrations/0004_allow_anonymous_distributions.py @@ -0,0 +1,115 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'MirrorLog.created' + db.alter_column('userpypi_mirrorlog', 'created', self.gf('django.db.models.fields.DateTimeField')()) + + + def backwards(self, orm): + + # Changing field 'MirrorLog.created' + db.alter_column('userpypi_mirrorlog', 'created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True)) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'userpypi.classifier': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Classifier'}, + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}) + }, + 'userpypi.distribution': { + 'Meta': {'unique_together': "(('release', 'filetype', 'pyversion'),)", 'object_name': 'Distribution'}, + 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'filetype': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'md5_digest': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'pyversion': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'distributions'", 'to': "orm['userpypi.Release']"}), + 'signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'uploader': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'blank': 'True'}) + }, + 'userpypi.masterindex': { + 'Meta': {'object_name': 'MasterIndex'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'userpypi.mirrorlog': { + 'Meta': {'object_name': 'MirrorLog'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'default': "'now'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'master': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'logs'", 'to': "orm['userpypi.MasterIndex']"}), + 'releases_added': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'mirror_sources'", 'blank': 'True', 'to': "orm['userpypi.Release']"}) + }, + 'userpypi.package': { + 'Meta': {'ordering': "['name']", 'object_name': 'Package'}, + 'allow_comments': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'auto_hide': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'maintainers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'packages_maintained'", 'blank': 'True', 'to': "orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'primary_key': 'True'}), + 'owners': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'packages_owned'", 'blank': 'True', 'to': "orm['auth.User']"}) + }, + 'userpypi.release': { + 'Meta': {'ordering': "['-created']", 'unique_together': "(('package', 'version'),)", 'object_name': 'Release'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'metadata_version': ('django.db.models.fields.CharField', [], {'default': "'1.0'", 'max_length': '64'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'releases'", 'to': "orm['userpypi.Package']"}), + 'package_info': ('userpypi.models.PackageInfoField', [], {}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'userpypi.review': { + 'Meta': {'object_name': 'Review'}, + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'rating': ('django.db.models.fields.PositiveSmallIntegerField', [], {'blank': 'True'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reviews'", 'to': "orm['userpypi.Release']"}) + } + } + + complete_apps = ['userpypi'] diff --git a/userpypi/migrations/0005_allow_null_distribution_uploader.py b/userpypi/migrations/0005_allow_null_distribution_uploader.py new file mode 100644 index 0000000..860145a --- /dev/null +++ b/userpypi/migrations/0005_allow_null_distribution_uploader.py @@ -0,0 +1,115 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'Distribution.uploader' + db.alter_column('userpypi_distribution', 'uploader_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True)) + + + def backwards(self, orm): + + # User chose to not deal with backwards NULL issues for 'Distribution.uploader' + raise RuntimeError("Cannot reverse this migration. 'Distribution.uploader' and its values cannot be restored.") + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'userpypi.classifier': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Classifier'}, + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}) + }, + 'userpypi.distribution': { + 'Meta': {'unique_together': "(('release', 'filetype', 'pyversion'),)", 'object_name': 'Distribution'}, + 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'filetype': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'md5_digest': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'pyversion': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'distributions'", 'to': "orm['userpypi.Release']"}), + 'signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'uploader': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}) + }, + 'userpypi.masterindex': { + 'Meta': {'object_name': 'MasterIndex'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'userpypi.mirrorlog': { + 'Meta': {'object_name': 'MirrorLog'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'default': "'now'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'master': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'logs'", 'to': "orm['userpypi.MasterIndex']"}), + 'releases_added': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'mirror_sources'", 'blank': 'True', 'to': "orm['userpypi.Release']"}) + }, + 'userpypi.package': { + 'Meta': {'ordering': "['name']", 'object_name': 'Package'}, + 'allow_comments': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'auto_hide': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'maintainers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'packages_maintained'", 'blank': 'True', 'to': "orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'primary_key': 'True'}), + 'owners': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'packages_owned'", 'blank': 'True', 'to': "orm['auth.User']"}) + }, + 'userpypi.release': { + 'Meta': {'ordering': "['-created']", 'unique_together': "(('package', 'version'),)", 'object_name': 'Release'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'metadata_version': ('django.db.models.fields.CharField', [], {'default': "'1.0'", 'max_length': '64'}), + 'package': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'releases'", 'to': "orm['userpypi.Package']"}), + 'package_info': ('userpypi.models.PackageInfoField', [], {}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'userpypi.review': { + 'Meta': {'object_name': 'Review'}, + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'rating': ('django.db.models.fields.PositiveSmallIntegerField', [], {'blank': 'True'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reviews'", 'to': "orm['userpypi.Release']"}) + } + } + + complete_apps = ['userpypi'] diff --git a/chishop/media/dists/__init__.py b/userpypi/migrations/__init__.py similarity index 100% rename from chishop/media/dists/__init__.py rename to userpypi/migrations/__init__.py diff --git a/userpypi/models.py b/userpypi/models.py new file mode 100644 index 0000000..4d7b659 --- /dev/null +++ b/userpypi/models.py @@ -0,0 +1,227 @@ +import os +from django.conf import settings +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.utils import simplejson as json +from django.utils.datastructures import MultiValueDict +from django.contrib.auth.models import User + +from userpypi.settings import (RELEASE_UPLOAD_TO, DIST_FILE_TYPES, + PYTHON_VERSIONS, DIST_FILE_TYPES, RELEASE_FILE_STORAGE) + +from django.core.files.storage import get_storage_class + +FILE_STORAGE = get_storage_class(RELEASE_FILE_STORAGE) + +class PackageInfoField(models.Field): + description = u'Python Package Information Field' + __metaclass__ = models.SubfieldBase + + def __init__(self, *args, **kwargs): + kwargs['editable'] = False + super(PackageInfoField,self).__init__(*args, **kwargs) + + def to_python(self, value): + if isinstance(value, basestring): + if value: + return MultiValueDict(json.loads(value)) + else: + return MultiValueDict() + if isinstance(value, dict): + return MultiValueDict(value) + if isinstance(value,MultiValueDict): + return value + raise ValueError('Unexpected value encountered when converting data to python') + + def get_prep_value(self, value): + if isinstance(value,MultiValueDict): + return json.dumps(dict(value.iterlists())) + if isinstance(value, dict): + return json.dumps(value) + if isinstance(value, basestring) or value is None: + return value + + raise ValueError('Unexpected value encountered when preparing for database') + + def get_internal_type(self): + return 'TextField' + +class Classifier(models.Model): + name = models.CharField(max_length=255, primary_key=True) + + class Meta: + verbose_name = _(u"classifier") + verbose_name_plural = _(u"classifiers") + ordering = ('name',) + + def __unicode__(self): + return self.name + + +class Package(models.Model): + owner = models.ForeignKey(User, related_name="packages_owned") + name = models.CharField(max_length=255) + auto_hide = models.BooleanField(_(u"Auto hide"), + default=True, + blank=False, + help_text="""Automatically hide previous releases when new releases + are created.""") + maintainers = models.ManyToManyField( + User, + blank=True, + related_name="packages_maintained", + through='Maintainer') + private = models.BooleanField(default=True) + + class Meta: + verbose_name = _(u"package") + verbose_name_plural = _(u"packages") + get_latest_by = "releases__latest" + ordering = ['name',] + unique_together = ('owner', 'name',) + permissions = ( + ('read_packages', 'Read Packages'), + ('update_packages', 'Update Packages'), + ('create_packages', 'Create Packages'), + ('admin', 'Administrator'), + ) + + def __unicode__(self): + return self.name + + @models.permalink + def get_absolute_url(self): + return ('userpypi-package', (), { + 'owner': self.owner.username, + 'package': self.name + }) + + @property + def latest(self): + try: + return self.releases.latest() + except Release.DoesNotExist: + return None + + def get_release(self, version): + """Return the release object for version, or None""" + try: + return self.releases.get(version=version) + except Release.DoesNotExist: + return None + +class Maintainer(models.Model): + package = models.ForeignKey(Package) + user = models.ForeignKey(User) + permission = models.BigIntegerField(choices=enumerate(['Read Only', 'Read and Write'])) + + +class Release(models.Model): + package = models.ForeignKey(Package, related_name="releases", editable=False) + version = models.CharField(max_length=128, editable=False) + metadata_version = models.CharField(max_length=64, default='1.0') + package_info = PackageInfoField(blank=False) + hidden = models.BooleanField(default=False) + created = models.DateTimeField(auto_now_add=True, editable=False) + + class Meta: + verbose_name = _(u"release") + verbose_name_plural = _(u"releases") + unique_together = ("package", "version") + get_latest_by = 'created' + ordering = ['-created'] + + def __unicode__(self): + return self.release_name + + @property + def release_name(self): + return u"%s-%s" % (self.package.name, self.version) + + @property + def summary(self): + return self.package_info.get('summary', u'') + + @property + def description(self): + return self.package_info.get('description', u'') + + @property + def classifiers(self): + return self.package_info.getlist('classifier') + + @models.permalink + def get_absolute_url(self): + return ('userpypi-release', (), { + 'owner': selfpackage.owner.username, + 'package': self.package.name, + 'version': self.version + }) + + +class Distribution(models.Model): + release = models.ForeignKey(Release, related_name="distributions", + editable=False) + content = models.FileField(upload_to=RELEASE_UPLOAD_TO, storage=FILE_STORAGE()) + md5_digest = models.CharField(max_length=32, blank=True, editable=False) + filetype = models.CharField(max_length=32, blank=False, + choices=DIST_FILE_TYPES) + pyversion = models.CharField(max_length=16, blank=True, + choices=PYTHON_VERSIONS) + comment = models.CharField(max_length=255, blank=True) + signature = models.TextField(blank=True) + created = models.DateTimeField(auto_now_add=True, editable=False) + uploader = models.ForeignKey(User, editable=False, blank=True, null=True) + + @property + def filename(self): + return os.path.basename(self.content.name) + + @property + def display_filetype(self): + for key,value in DIST_FILE_TYPES: + if key == self.filetype: + return value + return self.filetype + + @property + def path(self): + return self.content.name + + def get_absolute_url(self): + return "%s#md5=%s" % (self.content.url, self.md5_digest) + + class Meta: + verbose_name = _(u"distribution") + verbose_name_plural = _(u"distributions") + unique_together = ("release", "filetype", "pyversion") + + def __unicode__(self): + return self.filename + + +try: + from south.modelsinspector import add_introspection_rules + add_introspection_rules([], ["^userpypi\.models\.PackageInfoField"]) +except ImportError: + pass + + +class MasterIndex(models.Model): + title = models.CharField(max_length=255) + url = models.CharField(max_length=255) + + def __unicode__(self): + return self.title + +class MirrorLog(models.Model): + master = models.ForeignKey(MasterIndex, related_name='logs') + created = models.DateTimeField(default='now') + releases_added = models.ManyToManyField(Release, blank=True, + related_name='mirror_sources') + + def __unicode__(self): + return '%s (%s)' % (self.master, str(self.created),) + + class Meta: + get_latest_by = "created" diff --git a/userpypi/search_indexes.py b/userpypi/search_indexes.py new file mode 100644 index 0000000..8e2120b --- /dev/null +++ b/userpypi/search_indexes.py @@ -0,0 +1,36 @@ +from django.conf import settings + +from userpypi.models import Package + +if 'haystack' in settings.INSTALLED_APPS: + from haystack import site + from haystack.indexes import SearchIndex + from haystack.fields import CharField, MultiValueField + + class PackageSearchIndex(SearchIndex): + name = CharField(model_attr='name') + text = CharField(document=True, use_template=True, null=True, stored=False, + template_name='userpypi/haystack/package_text.txt') + author = MultiValueField(stored=False, null=True) + classifier = MultiValueField(stored=False, null=True, + model_attr='latest__classifiers') + summary = CharField(stored=False, null=True, + model_attr='latest__summary') + description = CharField(stored=False, null=True, + model_attr='latest__description') + + def prepare_author(self, obj): + output = [] + for user in list(obj.owners.all()) + list(obj.maintainers.all()): + output.append(user.get_full_name()) + if user.email: + output.append(user.email) + if obj.latest: + info = obj.latest.package_info + for field in ('author','author_email', 'maintainer', + 'maintainer_email',): + if info.get(field): + output.append(info.get(field)) + return output + + site.register(Package, PackageSearchIndex) diff --git a/userpypi/settings.py b/userpypi/settings.py new file mode 100644 index 0000000..9c7ca72 --- /dev/null +++ b/userpypi/settings.py @@ -0,0 +1,124 @@ +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.utils.importlib import import_module + +import warnings + +DEFAULT_SETTINGS = { + 'ALLOW_VERSION_OVERWRITE': False, # This is disabled on pypi.python.org, can be useful if you make mistakes + 'RELEASE_UPLOAD_TO': 'dists', # The upload_to argument for the file field in releases. This can either be a string for a path relative to your media folder or a callable. + 'RELEASE_FILE_STORAGE': settings.DEFAULT_FILE_STORAGE, + 'OS_NAMES': ( + ("aix", "AIX"), + ("beos", "BeOS"), + ("debian", "Debian Linux"), + ("dos", "DOS"), + ("freebsd", "FreeBSD"), + ("hpux", "HP/UX"), + ("mac", "Mac System x."), + ("macos", "MacOS X"), + ("mandrake", "Mandrake Linux"), + ("netbsd", "NetBSD"), + ("openbsd", "OpenBSD"), + ("qnx", "QNX"), + ("redhat", "RedHat Linux"), + ("solaris", "SUN Solaris"), + ("suse", "SuSE Linux"), + ("yellowdog", "Yellow Dog Linux"), + ), + 'ARCHITECTURES': ( + ("alpha", "Alpha"), + ("hppa", "HPPA"), + ("ix86", "Intel"), + ("powerpc", "PowerPC"), + ("sparc", "Sparc"), + ("ultrasparc", "UltraSparc"), + ), + 'DIST_FILE_TYPES': ( + ('sdist','Source'), + ('bdist_dumb','"dumb" binary'), + ('bdist_rpm','RPM'), + ('bdist_wininst','MS Windows installer'), + ('bdist_egg','Python Egg'), + ('bdist_dmg','OS X Disk Image'), + ), + 'PYTHON_VERSIONS': ( + ('any','Any i.e. pure python'), + ('2.1','2.1'), + ('2.2','2.2'), + ('2.3','2.3'), + ('2.4','2.4'), + ('2.5','2.5'), + ('2.6','2.6'), + ('2.7','2.7'), + ('3.0','3.0'), + ('3.1','3.1'), + ('3.2','3.2'), + ), + 'METADATA_FIELDS': { + '1.0': ('platform','summary','description','keywords','home_page', + 'author','author_email', 'license', ), + '1.1': ('platform','supported_platform','summary','description', + 'keywords','home_page','download_url','author','author_email', + 'license','classifier','requires','provides','obsoletes',), + '1.2': ('platform','supported_platform','summary','description', + 'keywords','home_page','download_url','author','author_email', + 'maintainer','maintainer_email','license','classifier', + 'requires_dist','provides_dist','obsoletes_dist', + 'requires_python','requires_external','project_url') + }, + 'METADATA_FORMS': { + '1.0': 'userpypi.forms.Metadata10Form', + '1.1': 'userpypi.forms.Metadata11Form', + '1.2': 'userpypi.forms.Metadata12Form' + }, + 'FALLBACK_VIEW': 'userpypi.views.releases.ReleaseListView', + 'ACTION_VIEWS': { + "file_upload": 'userpypi.views.distutils.register_or_upload', #``sdist`` command + "submit": 'userpypi.views.distutils.register_or_upload', #``register`` command + "list_classifiers": 'userpypi.views.distutils.list_classifiers', #``list_classifiers`` command + }, + 'XMLRPC_COMMANDS': { + 'list_packages': 'userpypi.views.xmlrpc.list_packages', + 'package_releases': 'userpypi.views.xmlrpc.package_releases', + 'release_urls': 'userpypi.views.xmlrpc.release_urls', + 'release_data': 'userpypi.views.xmlrpc.release_data', + #'search': xmlrpc.search, Not done yet + #'changelog': xmlrpc.changelog, Not done yet + #'ratings': xmlrpc.ratings, Not done yet + }, + 'PROXY_BASE_URL': 'http://pypi.python.org/simple', + 'PROXY_MISSING': False, + 'MIRRORING': False, +} + +USER_SETTINGS = DEFAULT_SETTINGS.copy() +USER_SETTINGS.update(getattr(settings, 'DJANGOPYPI_SETTINGS', {})) + +ORIGINAL_SETTINGS = ( + 'DJANGOPYPI_ALLOW_VERSION_OVERWRITE', + 'DJANGOPYPI_RELEASE_UPLOAD_TO', + 'DJANGOPYPI_OS_NAMES', + 'DJANGOPYPI_ARCHITECTURES', + 'DJANGOPYPI_DIST_FILE_TYPES', + 'DJANGOPYPI_PYTHON_VERSIONS', + 'DJANGOPYPI_METADATA_FIELDS', + 'DJANGOPYPI_METADATA_FORMS', + 'DJANGOPYPI_FALLBACK_VIEW', + 'DJANGOPYPI_ACTION_VIEWS', + 'DJANGOPYPI_XMLRPC_COMMANDS', + 'DJANGOPYPI_PROXY_BASE_URL', + 'DJANGOPYPI_PROXY_MISSING', + 'DJANGOPYPI_MIRRORING', +) + +for setting in ORIGINAL_SETTINGS: + value = getattr(settings, setting, False) + if value: + message = "%s is deprecated. Please use DJANGOPYPI_SETTINGS[%s] instead." + new_setting = setting.replace('DJANGOPYPI_', '') + warnings.warn(message % (setting, new_setting), DeprecationWarning) + USER_SETTINGS[new_setting] = value + globals()[setting] = value + +globals().update(USER_SETTINGS) diff --git a/userpypi/signals.py b/userpypi/signals.py new file mode 100644 index 0000000..55a06bf --- /dev/null +++ b/userpypi/signals.py @@ -0,0 +1,59 @@ +from django.db.models import signals +from django.utils.hashcompat import md5_constructor + +from userpypi.models import Package, Release, Distribution + + +def autohide_new_release_handler(sender, instance, created, *args, **kwargs): + """ Autohide other releases on the creation of a new release when the + package 'auto-hide' is True""" + if not created or not instance.package.auto_hide: + return + + for release in instance.package.releases.exclude(pk=instance.pk).filter(hidden=False): + release.hidden = True + release.save() + + if instance.hidden: + instance.hidden = False + instance.save() + +def autohide_save_release_handler(sender, instance, *args, **kwargs): + """ When saving a release, check to see if it should be hidden or not """ + if instance.pk is None: + return + + if not instance.package.auto_hide: + return + + try: + latest = instance.package.releases.latest('created') + except Release.DoesNotExist: + return + + if instance != latest and not instance.hidden: + instance.hidden = True + +def autohide_save_package_handler(sender, instance, *args, **kwargs): + if not instance.auto_hide: + return + + for release in instance.releases.filter(hidden=False): + release.save() + +def distribution_hash(sender, instance, *args, **kwargs): + if not instance.md5_digest and instance.content: + digest = md5_constructor() + try: + fh = instance.content.storage.open(instance.content.name) + map(digest.update,fh.readlines()) + fh.close() + instance.md5_digest = digest.hexdigest() + instance.save() + except Exception, e: + print str(e) + +signals.post_save.connect(autohide_new_release_handler, sender=Release) +signals.pre_save.connect(autohide_save_release_handler, sender=Release) +signals.pre_save.connect(autohide_save_package_handler, sender=Package) +signals.post_save.connect(distribution_hash, sender=Distribution) diff --git a/userpypi/templates/djangopypi/haystack/package_text.txt b/userpypi/templates/djangopypi/haystack/package_text.txt new file mode 100644 index 0000000..ad0bfac --- /dev/null +++ b/userpypi/templates/djangopypi/haystack/package_text.txt @@ -0,0 +1,8 @@ +{{ object.name }} + +{% if object.latest %} +{{ object.latest.release_name }} +{% for key, list in object.latest.package_info.iterlists %} +{{ key }}: {% for item in list %}{{ item }}{% if not forloop.last %}; {% endif %}{% endfor %} +{% endfor %} +{% endif %} diff --git a/userpypi/templates/djangopypi/package_detail.html b/userpypi/templates/djangopypi/package_detail.html new file mode 100644 index 0000000..55014d8 --- /dev/null +++ b/userpypi/templates/djangopypi/package_detail.html @@ -0,0 +1,29 @@ + + + {{ package.name }} + + + +

{{ package.name }}

+ {% if not package.latest %} +
No releases yet!
+ {% endif %} + {% if package.latest %} + {% with package.latest as release %} + {% load safemarkup %} + {{ release.description|saferst }} + + {% if release.distributions.count %} +

Downloads

+
    + {% for dist in release.distributions.all %} +
  • {{ dist }} ({{ dist.content.size|filesizeformat }})
  • + {% endfor %} +
+ {% endif %} + + {% endwith %} + {% endif %} + + \ No newline at end of file diff --git a/userpypi/templates/djangopypi/package_detail_simple.html b/userpypi/templates/djangopypi/package_detail_simple.html new file mode 100644 index 0000000..b8eaa8c --- /dev/null +++ b/userpypi/templates/djangopypi/package_detail_simple.html @@ -0,0 +1,14 @@ + + +Links for {{ package.name }} + + +

Links for {{ package.name }}

+{% for release in package.releases.all %} +{% for dist in release.distributions.all %} +{{ dist.filename }}
{% endfor %} +{% if release.package_info.home_page %}{{ release.version }} home-page
{% endif %} +{% if release.package_info.download_url %}{{ release.version }} download-url
{% endif %} +{% endfor %} + + \ No newline at end of file diff --git a/userpypi/templates/djangopypi/package_doap.xml b/userpypi/templates/djangopypi/package_doap.xml new file mode 100644 index 0000000..b1601f1 --- /dev/null +++ b/userpypi/templates/djangopypi/package_doap.xml @@ -0,0 +1,59 @@ + + + {{ package.name }} + {% for release in package.releases.all %}{% if forloop.last %} + {{ release.created|date:"Y-m-d" }} + {% endif %}{% endfor %} + + {% if package.latest %} + {% with package.latest as release %} + {{ release.summary }} + {% if release.description %} + + {{ release.description }} + + {% endif %} + {% if release.package_info.home_page %} + + {% endif %} + {% if release.package_info.download_url %} + + {% endif %} + {% if release.package_info.author or release.package_info.author_email %} + + + {% if release.package_info.author %}{{ release.package_info.author }}{% endif %} + {% if release.package_info.author_email %}{% endif %} + + + {% endif %} + {% if release.package_info.maintainer or release.package_info.maintainer_email %} + + + {% if release.package_info.maintainer %}{{ release.package_info.maintainer }}{% endif %} + {% if release.package_info.maintainer_email %}{% endif %} + + + {% endif %} + {% if release.package_info.license %} + {{ release.package_info.license }} + {% endif %} + {% if release.classifiers %} + {% for classifier in release.classifiers %} + {{ classifier }} + {% endfor %} + {% endif %} + {% endwith %} + {% endif %} + {% if release %} + {% include "userpypi/release_doap_fragment.xml" %} + {% else %} + {% for release in package.releases.all %} + {% include "userpypi/release_doap_fragment.xml" %} + {% endfor %} + {% endif %} + diff --git a/userpypi/templates/djangopypi/package_list.html b/userpypi/templates/djangopypi/package_list.html new file mode 100644 index 0000000..98c4bd4 --- /dev/null +++ b/userpypi/templates/djangopypi/package_list.html @@ -0,0 +1,13 @@ + + + Package Index + + +

Package Index

+
    + {% for package in package_list %} +
  • {{ package.name }}{% if package.latest and package.latest.summary %}: {{ package.latest.summary }}{% endif %}
  • + {% endfor %} +
+ + \ No newline at end of file diff --git a/userpypi/templates/djangopypi/package_list_simple.html b/userpypi/templates/djangopypi/package_list_simple.html new file mode 100644 index 0000000..233434d --- /dev/null +++ b/userpypi/templates/djangopypi/package_list_simple.html @@ -0,0 +1,10 @@ + + +Simple Package Index + + +{% for package in package_list %} +{{ package.name }}
+{% endfor %} + + \ No newline at end of file diff --git a/userpypi/templates/djangopypi/package_manage.html b/userpypi/templates/djangopypi/package_manage.html new file mode 100644 index 0000000..1966e99 --- /dev/null +++ b/userpypi/templates/djangopypi/package_manage.html @@ -0,0 +1,13 @@ + + + Manage {{ package.name }} + + +

Manage {{ package.name }}

+
    + + {{ form.as_p }} + +
+ + \ No newline at end of file diff --git a/userpypi/templates/djangopypi/package_manage_versions.html b/userpypi/templates/djangopypi/package_manage_versions.html new file mode 100644 index 0000000..4330af9 --- /dev/null +++ b/userpypi/templates/djangopypi/package_manage_versions.html @@ -0,0 +1,37 @@ + + + Manage {{ package.name }} Versions + + +

Manage {{ package.name }} Versions

+ + {{ formset.management_form }} + + + + + + + + + + + {% for form in formset.forms %} + {% for field in form %}{% if field.is_hidden %}{{ field }}{% endif %}{% endfor %} + {% with form.instance as release %} + + + + + + + + + {% endwith %} + {% endfor %} + +
Remove?VersionHide?Links
{{ form.DELETE }}{{ release.version }}{{ form.hidden }}ShowEditFiles
+
+ + + \ No newline at end of file diff --git a/userpypi/templates/djangopypi/release_detail.html b/userpypi/templates/djangopypi/release_detail.html new file mode 100644 index 0000000..c34cf1b --- /dev/null +++ b/userpypi/templates/djangopypi/release_detail.html @@ -0,0 +1,25 @@ + + + {{ release }} + + + +

{{ release }}

+ {% ifnotequal release release.package.latest %} + + {% endifnotequal %} + {% load safemarkup %} + {{ release.description|saferst }} + + {% if release.distributions.count %} +

Downloads

+
    + {% for dist in release.distributions.all %} +
  • {{ dist }} ({{ dist.content.size|filesizeformat }})
  • + {% endfor %} +
+ {% endif %} + + + \ No newline at end of file diff --git a/userpypi/templates/djangopypi/release_doap.xml b/userpypi/templates/djangopypi/release_doap.xml new file mode 100644 index 0000000..24759c1 --- /dev/null +++ b/userpypi/templates/djangopypi/release_doap.xml @@ -0,0 +1,2 @@ +{% with release.package as package %}{% include "userpypi/package_doap.xml" %}{% endwith %} + diff --git a/userpypi/templates/djangopypi/release_doap_fragment.xml b/userpypi/templates/djangopypi/release_doap_fragment.xml new file mode 100644 index 0000000..33d4119 --- /dev/null +++ b/userpypi/templates/djangopypi/release_doap_fragment.xml @@ -0,0 +1,10 @@ + + + {{ release.package.name }} + {{ release.created|date:"Y-m-d" }} + {{ release.version }} + {% for dist in release.distributions.all %} + {{ dist.filename }} + {% endfor %} + + \ No newline at end of file diff --git a/userpypi/templates/djangopypi/release_list.html b/userpypi/templates/djangopypi/release_list.html new file mode 100644 index 0000000..77d6b02 --- /dev/null +++ b/userpypi/templates/djangopypi/release_list.html @@ -0,0 +1,21 @@ + + + Package Index Releases + + + + + + + + {% for release in release_list %} + + + + + {% endfor %} + +
UpdatedPackageDescription
{{ release.created|date:"Y-m-d" }} + {{request.user}} {{release.package.name}} {{release.version}} {{ release }}{{ release.summary|truncatewords:10 }}
+ + \ No newline at end of file diff --git a/userpypi/templates/djangopypi/release_manage.html b/userpypi/templates/djangopypi/release_manage.html new file mode 100644 index 0000000..5e8c47f --- /dev/null +++ b/userpypi/templates/djangopypi/release_manage.html @@ -0,0 +1,13 @@ + + + Manage {{ release }} + + +

Manage {{ release }}

+
    +
    + {{ form.as_p }} + +
+ + \ No newline at end of file diff --git a/userpypi/templates/djangopypi/release_manage_files.html b/userpypi/templates/djangopypi/release_manage_files.html new file mode 100644 index 0000000..f629fc7 --- /dev/null +++ b/userpypi/templates/djangopypi/release_manage_files.html @@ -0,0 +1,51 @@ + + + Manage {{ release }} Files + + +

Manage {{ release }} Files

+ {% if formset.forms %} + + {{ formset.management_form }} + + + + + + + + + + + + + + {% for form in formset.forms %} + {% for field in form %}{% if field.is_hidden %}{{ field }}{% endif %}{% endfor %} + {% with form.instance as dist %} + + + + + + + + + + {% endwith %} + {% endfor %} + +
Remove?TypePy VersionCommentDownloadSizeMD5 Digest
{{ form.DELETE }}{{ dist.get_filetype_display }}{{ dist.pyversion }}{{ form.comment }}{{ dist.filename }}{{ dist.content.size|filesizeformat }}{{ dist.md5_digest }}
+
+ + {% endif %} + {% if upload_form %} +

Upload a Distribution

+
+ {{ upload_form.as_p }} +
+
+ {% endif %} + + \ No newline at end of file diff --git a/userpypi/templates/djangopypi/release_upload_file.html b/userpypi/templates/djangopypi/release_upload_file.html new file mode 100644 index 0000000..724a80c --- /dev/null +++ b/userpypi/templates/djangopypi/release_upload_file.html @@ -0,0 +1,12 @@ + + + Manage {{ release }} Files + + +

Upload a File to {{ release }}

+
+ {{ form.as_p }} +
+
+ + \ No newline at end of file diff --git a/djangopypi/management/__init__.py b/userpypi/templatetags/__init__.py similarity index 100% rename from djangopypi/management/__init__.py rename to userpypi/templatetags/__init__.py diff --git a/djangopypi/templatetags/safemarkup.py b/userpypi/templatetags/safemarkup.py similarity index 100% rename from djangopypi/templatetags/safemarkup.py rename to userpypi/templatetags/safemarkup.py diff --git a/userpypi/tests/__init__.py b/userpypi/tests/__init__.py new file mode 100644 index 0000000..9498d8f --- /dev/null +++ b/userpypi/tests/__init__.py @@ -0,0 +1,212 @@ +import os +import unittest +import xmlrpclib +import StringIO +#from userpypi.views import parse_distutils_request, simple +from userpypi.models import Package, Classifier, Release, PackageInfoField, Distribution +from django.test.client import Client +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from django.http import HttpRequest + +def create_post_data(action): + data = { + ":action": action, + "metadata_version": "1.0", + "name": "foo", + "version": "0.1.0-pre2", + "summary": "The quick brown fox jumps over the lazy dog.", + "home_page": "http://example.com", + "author": "Foo Bar Baz", + "author_email": "foobarbaz@example.com", + "license": "Apache", + "keywords": "foo bar baz", + "platform": "UNKNOWN", + "classifiers": [ + "Development Status :: 3 - Alpha", + "Environment :: Web Environment", + "Framework :: Django", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: BSD License", + "Topic :: System :: Software Distribution", + "Programming Language :: Python", + ], + "download_url": "", + "provides": "", + "requires": "", + "obsoletes": "", + "description": """ +========= +FOOBARBAZ +========= + +Introduction +------------ + ``foo`` :class:`bar` + *baz* + [foaoa] + """, + } + return data + +def create_request(data): + boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' + sep_boundary = '\n--' + boundary + end_boundary = sep_boundary + '--' + body = StringIO.StringIO() + for key, value in data.items(): + # handle multiple entries for the same name + if type(value) not in (type([]), type( () )): + value = [value] + for value in value: + value = unicode(value).encode("utf-8") + body.write(sep_boundary) + body.write('\nContent-Disposition: form-data; name="%s"'%key) + body.write("\n\n") + body.write(value) + if value and value[-1] == '\r': + body.write('\n') # write an extra newline (lurve Macs) + body.write(end_boundary) + body.write("\n") + + return body.getvalue() + + +# class MockRequest(object): +# +# def __init__(self, raw_post_data): +# self.raw_post_data = raw_post_data +# self.META = {} +# +# +# class TestParseWeirdPostData(unittest.TestCase): +# +# def test_weird_post_data(self): +# data = create_post_data("submit") +# raw_post_data = create_request(data) +# post, files = parse_distutils_request(MockRequest(raw_post_data)) +# self.assertTrue(post) +# +# for key in post.keys(): +# if isinstance(data[key], list): +# self.assertEquals(data[key], post.getlist(key)) +# elif data[key] == "UNKNOWN": +# self.assertTrue(post[key] is None) +# else: +# self.assertEquals(post[key], data[key]) + +client = Client() + +class TestSearch(unittest.TestCase): + + def setUp(self): + self.dummy_user = User.objects.create(username='krill', password='12345', + email='krill@opera.com') + self.pkg = Package.objects.create(name='foo') + self.pkg.owners.add(self.dummy_user) + + def tearDown(self): + self.pkg.delete() + self.dummy_user.delete() + + def test_search_for_package(self): + response = client.post(reverse('userpypi-search'), {'search_term': 'foo'}) + self.assertTrue("The quick brown fox jumps over the lazy dog." in response.content) + +class TestSimpleView(unittest.TestCase): + + def create_distutils_httprequest(self, user_data={}): + self.post_data = create_post_data(action='user') + self.post_data.update(user_data) + self.raw_post_data = create_request(self.post_data) + request = HttpRequest() + request.POST = self.post_data + request.method = "POST" + request.raw_post_data = self.raw_post_data + return request + + # def test_user_registration(self): + # request = self.create_distutils_httprequest({'name': 'peter_parker', 'email':'parker@dailybugle.com', + # 'password':'spiderman'}) + # response = simple(request) + # self.assertEquals(200, response.status_code) + # + # def test_user_registration_with_wrong_data(self): + # request = self.create_distutils_httprequest({'name': 'peter_parker', 'email':'parker@dailybugle.com', + # 'password':'',}) + # response = simple(request) + # self.assertEquals(400, response.status_code) + +from django.test.client import MULTIPART_CONTENT +class XmlRpcClient(Client): + def __init__(self, *args, **kwargs): + self.extra_headers = {} + super(XmlRpcClient, self).__init__(*args, **kwargs) + + def putheader(self, key, value): + self.extra_headers[key] = value + def endheaders(self, request_body): + pass + def getresponse(self, buffering=True): + return self.response + def post(self, path, data={}, content_type="text/xml", + follow=False, **extra): + """ + Requests a response from the server using POST. + """ + extra.update(self.extra_headers) + response = super(XmlRpcClient, self).post(path, data, content_type, follow, **extra) + return response.content + + +class ProxiedTransport(xmlrpclib.Transport): + def set_proxy(self, proxy): + self.proxy = proxy + def make_connection(self, host): + return XmlRpcClient() + def single_request(self, host, handler, request_body, verbose=0): + connection = self.make_connection(host) + response = connection.post('/pypi/', request_body) + data, methodname = xmlrpclib.loads(response) + return data + + def send_host(self, connection, host): + pass + + def send_user_agent(self, *args, **kwargs): + pass + +class TestXmlRpc(unittest.TestCase): + """ + Test that the server responds to xmlrpc requests + """ + def setUp(self): + from django.core.files.base import ContentFile + + self.dummy_file = ContentFile("gibberish") + self.dummy_user = User.objects.create(username='bobby', password='tables', + email='bobby@tables.com') + self.pkg = Package.objects.create(name='foo') + self.pkg.owners.add(self.dummy_user) + self.release = Release.objects.create(package=self.pkg, version="1.0") + + def tearDown(self): + self.release.delete() + self.pkg.delete() + self.dummy_user.delete() + + def test_list_package(self): + pypi = xmlrpclib.ServerProxy("http://localhost/pypi/", ProxiedTransport()) + pypi_hits = pypi.list_packages() + expected = ['foo'] + self.assertEqual(pypi_hits, expected) + + def test_package_releases(self): + pypi = xmlrpclib.ServerProxy("http://localhost/pypi/", ProxiedTransport()) + pypi_hits = pypi.package_releases('foo') + expected = ['1.0'] + self.assertEqual(pypi_hits, expected) + + \ No newline at end of file diff --git a/userpypi/urls.py b/userpypi/urls.py new file mode 100644 index 0000000..1857080 --- /dev/null +++ b/userpypi/urls.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +from django.conf.urls.defaults import patterns, url +from userpypi.feeds import ReleaseFeed +from userpypi.decorators import user_owns_package, basic_auth + +from .views.packages import PackageListView, PackageDetailView, manage +from .views.releases import ReleaseDetailView, ReleaseListView + +urlpatterns = patterns('', + + # Basic Package Indexes + url(r'^(?P[^/]+)/$', + PackageListView.as_view(), + name="userpypi-index"), + url(r'^(?P[^/]+)/packages/$', + PackageListView.as_view(), + name='userpypi-package-index'), + url(r'^(?P[^/]+)/pypi/releases/$', + ReleaseListView.as_view(), + name='userpypi-release-list'), + + url(r'^(?P[^/]+)/search/$', + 'userpypi.views.packages.search', + name='userpypi-search'), + url(r'^(?P[^/]+)/rss/$', + ReleaseFeed(), + name='userpypi-rss'), + + # Simple indexes + url(r'^(?P[^/]+)/simple/$', + basic_auth(PackageListView.as_view(simple=True)), + name='userpypi-package-index-simple'), + url(r'^(?P[^/]+)/simple/(?P[\w\d_\.\-]+)/?$', + basic_auth(PackageDetailView.as_view(simple=True)), + name='userpypi-package-simple'), + + # Regular Package Indexes + url(r'^(?P[^/]+)/pypi/$', + 'userpypi.views.root', + name="userpypi-root"), + url(r'^(?P[^/]+)/pypi/(?P[\w\d_\.\-]+)/?$', + basic_auth(PackageDetailView.as_view()), + name='userpypi-package'), + url(r'^(?P[^/]+)/pypi/(?P[\w\d_\.\-]+)/rss/$', + ReleaseFeed(), + name='userpypi-package-rss'), + url(r'^(?P[^/]+)/pypi/(?P[\w\d_\.\-]+)/doap.rdf$', + PackageDetailView.as_view(doap=True), + name='userpypi-package-doap'), + url(r'^(?P[^/]+)/pypi/(?P[\w\d_\.\-]+)/manage/$', + manage, + name='userpypi-package-manage'), + url(r'^pypi/(?P[\w\d_\.\-]+)/manage/versions/$', + 'userpypi.views.packages.manage_versions', + name='userpypi-package-manage-versions'), + + # Release Indexes + url(r'^(?P[^/]+)/pypi/(?P[\w\d_\.\-]+)/(?P[\w\d_\.\-]+)/$', + ReleaseDetailView.as_view(), + name='userpypi-release'), + url(r'^(?P[^/]+)/pypi/(?P[\w\d_\.\-]+)/(?P[\w\d_\.\-]+)/doap.rdf$', + ReleaseDetailView.as_view(doap=True), + name='userpypi-release-doap'), + url(r'^pypi/(?P[\w\d_\.\-]+)/(?P[\w\d_\.\-]+)/manage/$', + 'userpypi.views.releases.manage', + name='userpypi-release-manage'), + url(r'^pypi/(?P[\w\d_\.\-]+)/(?P[\w\d_\.\-]+)/metadata/$', + 'userpypi.views.releases.manage_metadata', + name='userpypi-release-manage-metadata'), + url(r'^pypi/(?P[\w\d_\.\-]+)/(?P[\w\d_\.\-]+)/files/$', + 'userpypi.views.releases.manage_files', + name='userpypi-release-manage-files'), + url(r'^pypi/(?P[\w\d_\.\-]+)/(?P[\w\d_\.\-]+)/files/upload/$', + 'userpypi.views.releases.upload_file', + name='userpypi-release-upload-file'), +) \ No newline at end of file diff --git a/userpypi/utils.py b/userpypi/utils.py new file mode 100644 index 0000000..48a58ec --- /dev/null +++ b/userpypi/utils.py @@ -0,0 +1,31 @@ +import sys, traceback +from django.conf import settings +from django.utils.importlib import import_module +from django.core.exceptions import ImproperlyConfigured + +def debug(func): + # @debug is handy when debugging distutils requests + if settings.DEBUG: + def _wrapped(*args, **kwargs): + try: + return func(*args, **kwargs) + except: + traceback.print_exception(*sys.exc_info()) + return _wrapped + else: + return func + +def get_class(import_path): + try: + dot = import_path.rindex('.') + except ValueError: + raise ImproperlyConfigured("'%s' isn't a valid python path." % import_path) + module, classname = import_path[:dot], import_path[dot+1:] + try: + mod = import_module(module) + except ImportError, e: + raise ImproperlyConfigured('Error importing module %s: "%s"' % (module, e)) + try: + return getattr(mod, classname) + except AttributeError: + raise ImproperlyConfigured('Module "%s" does not define "%s"' % (module, classname)) diff --git a/userpypi/views/__init__.py b/userpypi/views/__init__.py new file mode 100644 index 0000000..6f78bae --- /dev/null +++ b/userpypi/views/__init__.py @@ -0,0 +1,35 @@ +from django.http import HttpResponseNotAllowed + +from userpypi.decorators import csrf_exempt, basic_auth +from userpypi.http import parse_distutils_request +from userpypi.models import Package, Release +from userpypi.views.xmlrpc import parse_xmlrpc_request +from userpypi.settings import (FALLBACK_VIEW, ACTION_VIEWS) +from userpypi.utils import get_class, debug + +@csrf_exempt +@debug +@basic_auth +def root(request, fallback_view=None, **kwargs): + """ Root view of the package index, handle incoming actions from distutils + or redirect to a more user friendly view """ + + if request.method == 'POST': + if request.META['CONTENT_TYPE'] == 'text/xml': + return parse_xmlrpc_request(request) + parse_distutils_request(request) + action = request.POST.get(':action','') + else: + action = request.GET.get(':action','') + if not action: + if fallback_view is None: + fallback_view = get_class(FALLBACK_VIEW) + if hasattr(fallback_view, 'as_view'): + return fallback_view.as_view()(request, **kwargs) + return fallback_view(request, **kwargs) + + if not action in ACTION_VIEWS: + print 'unknown action: %s' % (action,) + return HttpResponseNotAllowed(ACTION_VIEWS.keys()) + + return get_class(ACTION_VIEWS[action])(request, **kwargs) diff --git a/userpypi/views/distutils.py b/userpypi/views/distutils.py new file mode 100644 index 0000000..47e1e9f --- /dev/null +++ b/userpypi/views/distutils.py @@ -0,0 +1,198 @@ +import os + +from django.db import transaction +from django.http import (HttpResponseForbidden, HttpResponseBadRequest, + HttpResponse, Http404) +from django.utils.translation import ugettext_lazy as _ +from django.utils.datastructures import MultiValueDict +from django.contrib.auth import login +from django.contrib.auth.models import User + +from userpypi.decorators import basic_auth +from userpypi.forms import PackageForm, ReleaseForm +from userpypi.models import Package, Release, Distribution, Classifier +from userpypi.settings import (ALLOW_VERSION_OVERWRITE, METADATA_FIELDS, RELEASE_UPLOAD_TO) + + +ALREADY_EXISTS_FMT = _( + "A file named '%s' already exists for %s. Please create a new release.") + +def submit_package_or_release(user, post_data, files): + """Registers/updates a package or release""" + try: + package = Package.objects.get(owner=user, name=post_data['name']) + except Package.DoesNotExist: + package = None + + package_form = PackageForm(post_data, instance=package) + if package_form.is_valid(): + package = package_form.save(commit=False) + package.owner = user + package.save() + for c in post_data.getlist('classifiers'): + classifier, created = Classifier.objects.get_or_create(name=c) + package.classifiers.add(classifier) + if files: + allow_overwrite = ALLOW_VERSION_OVERWRITE + try: + release = Release.objects.get(version=post_data['version'], + package=package, + distribution=RELEASE_UPLOAD_TO + '/' + + files['distribution']._name) + if not allow_overwrite: + return HttpResponseForbidden(ALREADY_EXISTS_FMT % ( + release.filename, release)) + except Release.DoesNotExist: + release = None + + # If the old file already exists, django will append a _ after the + # filename, however with .tar.gz files django does the "wrong" + # thing and saves it as package-0.1.2.tar_.gz. So remove it before + # django sees anything. + release_form = ReleaseForm(post_data, files, instance=release) + if release_form.is_valid(): + if release and os.path.exists(release.distribution.path): + os.remove(release.distribution.path) + release = release_form.save(commit=False) + release.package = package + release.save() + else: + return HttpResponseBadRequest( + "ERRORS: %s" % release_form.errors) + else: + return HttpResponseBadRequest("ERRORS: %s" % package_form.errors) + + return HttpResponse() + +def authorize(request_user, owner_user, package): + """ + Go through the checks to see if the user is authorized to perform any actions + + Returns: success, err_msg + """ + MUST_CREATE = package is not None + + if owner_user.profile.organization: + try: + membership = request_user.memberships.get(team=owner_user) + except request_user.DoesNotExist: + return False, 'You are not a member of team %s' % owner.username + if MUST_CREATE and membership.permission < 3: # Can't create a new package + return False, 'You can not create packages' + if membership.permission == 1: + return False, 'You can not update packages' + return True, '' + + if request_user != owner_user: + if MUST_CREATE: + return False, "You can not create a package on someone else's account." + try: + maintainer = package.maintainers.get(user=request_user) + except request_user.DoesNotExist: + return False, 'You are not a maintainer of %s' % package.name + if membership.permission == 1: + return False, 'You can not update packages' + return True, '' + +@basic_auth +@transaction.commit_manually +def register_or_upload(request, owner=None): + if request.method != 'POST': + transaction.rollback() + return HttpResponseBadRequest('Only post requests are supported') + + name = request.POST.get('name', None).strip() + if not name: + transaction.rollback() + return HttpResponseBadRequest('No package name specified') + + version = request.POST.get('version', None).strip() + metadata_version = request.POST.get('metadata_version', None).strip() + if not version or not metadata_version: + transaction.rollback() + return HttpResponseBadRequest( + 'Release version and metadata version must be specified') + if not metadata_version in METADATA_FIELDS: + transaction.rollback() + return HttpResponseBadRequest('Metadata version must be one of: %s' + (', '.join(METADATA_FIELDS.keys()),)) + + try: + owner_obj = User.objects.get(username=owner) + except User.DoesNotExist: + transaction.rollback() + raise Http404 + + try: + package = Package.objects.get(owner=owner_obj, name=name) + except Package.DoesNotExist: + package = None + + authorized, err_msg = authorize(request.user, owner_obj, package) + + if not authorized: + transaction.rollback() + return HttpResponseForbidden(err_msg) + + if package is None: + package = Package.objects.create(owner=owner_obj, name=name) + + release, created = Release.objects.get_or_create(package=package, + version=version) + + if (('classifiers' in request.POST or 'download_url' in request.POST) and + metadata_version == '1.0'): + metadata_version = '1.1' + + release.metadata_version = metadata_version + + fields = METADATA_FIELDS[metadata_version] + + if 'classifiers' in request.POST: + request.POST.setlist('classifier', request.POST.getlist('classifiers')) + + release.package_info = MultiValueDict(dict(filter(lambda t: t[0] in fields, + request.POST.iterlists()))) + + for key, value in release.package_info.iterlists(): + release.package_info.setlist(key, + filter(lambda v: v != 'UNKNOWN', value)) + + release.save() + + if not 'content' in request.FILES: + transaction.commit() + return HttpResponse('release registered') + + uploaded = request.FILES.get('content') + + for dist in release.distributions.all(): + if os.path.basename(dist.content.name) == uploaded.name: + """ Need to add handling optionally deleting old and putting up new """ + transaction.rollback() + return HttpResponseBadRequest('That file has already been uploaded...') + + md5_digest = request.POST.get('md5_digest','') + + try: + new_file = Distribution.objects.create(release=release, + content=uploaded, + filetype=request.POST.get('filetype','sdist'), + pyversion=request.POST.get('pyversion',''), + uploader=request.user, + comment=request.POST.get('comment',''), + signature=request.POST.get('gpg_signature',''), + md5_digest=md5_digest) + except Exception, e: + transaction.rollback() + print "Issue creating a Distribution", str(e) + raise + + transaction.commit() + + return HttpResponse('upload accepted') + +def list_classifiers(request, mimetype='text/plain'): + response = HttpResponse(mimetype=mimetype) + response.write(u'\n'.join(map(lambda c: c.name, Classifier.objects.all()))) + return response diff --git a/userpypi/views/packages.py b/userpypi/views/packages.py new file mode 100644 index 0000000..d2e7f46 --- /dev/null +++ b/userpypi/views/packages.py @@ -0,0 +1,196 @@ +from django.conf import settings +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist +from django.core.urlresolvers import reverse +from django.db.models.query import Q +from django.forms.models import inlineformset_factory +from django.http import Http404, HttpResponseRedirect, HttpResponse +from django.shortcuts import get_object_or_404, render_to_response +from django.template import RequestContext, loader +from django.utils.decorators import method_decorator +from django.views.generic import ListView, DetailView, UpdateView, create_update +from django.views.generic import ListView, DetailView, UpdateView + +from userpypi.decorators import user_owns_package, user_maintains_package +from userpypi.models import Package, Release +from userpypi.forms import SimplePackageSearchForm, PackageForm, MaintainerFormSet +from userpypi.settings import PROXY_MISSING, PROXY_BASE_URL + + +class OwnerObjectMixin(object): + def get_context_data(self, **kwargs): + context = super(OwnerObjectMixin, self).get_context_data(**kwargs) + context['owner'] = self.get_owner() + context['is_owner'] = self.owner == self.request.user.username + return context + + def get_owner(self): + """ + Set the owner user object + """ + owner = getattr(self, 'owner') + if owner and issubclass(owner.__class__, User): + return owner + owner = self.kwargs.get('owner', None) + if owner is not None: + self.owner = User.objects.get(username=owner) + else: + self.owner = None + return self.owner + + def get_queryset(self): + """ + Filter the queryset based on whether or not the requesting user is + the owner of the requested objects + """ + if self.request.user != self.get_owner(): + if self.owner.profile.organization: + params = dict(owner=self.owner) + else: + params = dict(owner=self.owner, private=False) + else: + params = dict(owner=self.owner) + return self.model.objects.filter(**params) + + +class PackageListView(OwnerObjectMixin, ListView): + model = Package + context_object_name = 'package_list' + simple = False + owner = None + + def get_template_names(self): + """ + Returns a list of template names to be used for the request. Must + return a list. May not be called if render_to_response is overridden. + """ + if self.simple: + return ['userpypi/package_list_simple.html'] + else: + return ['userpypi/package_list.html'] + + +class PackageDetailView(OwnerObjectMixin, DetailView): + model = Package + context_object_name = 'package' + simple = False + doap = False + owner = None + redirect = '' + + def render_to_response(self, context, **response_kwargs): + """ + Returns a response with a template rendered with the given context. + """ + if self.redirect: + return HttpResponseRedirect(self.redirect) + + self.doap = 'doap' in self.kwargs and self.kwargs['doap'] + + if self.doap: + response_kwargs['mimetype'] = 'text/xml' + + return super(PackageDetailView, self).render_to_response( + context, **response_kwargs) + + def get_object(self): + package = self.kwargs.get('package', None) + try: + queryset = self.get_queryset().filter(name=package) + obj = queryset.get() + except ObjectDoesNotExist: + if PROXY_MISSING: + self.redirect = '%s/%s/' % (PROXY_BASE_URL.rstrip('/'), package) + return None + raise Http404(u"No %(verbose_name)s found matching the query" % + {'verbose_name': queryset.model._meta.verbose_name}) + return obj + + def get_template_names(self): + """ + Returns a list of template names to be used for the request. Must + return a list. May not be called if render_to_response is overridden. + """ + if self.simple: + return ['userpypi/package_detail_simple.html'] + elif self.doap: + return ['userpypi/package_doap.xml'] + else: + return ['userpypi/package_detail.html'] + +@user_maintains_package() +def manage(request, owner, package): + """ + """ + template_name='userpypi/package_manage.html' + template_object_name='package' + form_class=PackageForm + obj = get_object_or_404(Package, owner__username=owner, name=package) + + if request.method == 'POST': + form = PackageForm(request.POST, instance=obj) + if form.is_valid(): + obj = form.save(commit=False) + maintainer_formset = MaintainerFormSet(request.POST, instance=obj) + if maintainer_formset.is_valid(): + obj.save() + maintainer_formset.save() + return HttpResponseRedirect(reverse('userpypi-package-manage', kwargs={'owner': owner, 'package': obj})) + else: + form = PackageForm(instance=obj) + maintainer_formset = MaintainerFormSet(instance=obj) + + t = loader.get_template(template_name) + c = RequestContext(request, { + 'form': form, + 'maintainer_formset': maintainer_formset, + 'package': obj, + }) + response = HttpResponse(t.render(c)) + return response + + +def search(request, **kwargs): + if request.method == 'POST': + form = SimplePackageSearchForm(request.POST) + else: + form = SimplePackageSearchForm(request.GET) + kwargs.pop('owner') + if form.is_valid(): + q = form.cleaned_data['query'] + kwargs['queryset'] = Package.objects.filter(owner=request.user).filter( + Q(name__contains=q) | Q(releases__package_info__contains=q)).distinct() + return PackageListView(request, **kwargs) + + +@user_maintains_package() +def manage_versions(request, package, **kwargs): + kwargs.pop('owner') + package = get_object_or_404(Package, owner=request.user, name=package) + kwargs.setdefault('formset_factory_kwargs', {}) + kwargs['formset_factory_kwargs'].setdefault('fields', ('hidden',)) + kwargs['formset_factory_kwargs']['extra'] = 0 + + kwargs.setdefault('formset_factory', inlineformset_factory(Package, Release, **kwargs['formset_factory_kwargs'])) + kwargs.setdefault('template_name', 'userpypi/package_manage_versions.html') + kwargs.setdefault('template_object_name', 'package') + kwargs.setdefault('extra_context', {}) + kwargs.setdefault('mimetype', settings.DEFAULT_CONTENT_TYPE) + kwargs['extra_context'][kwargs['template_object_name']] = package + kwargs.setdefault('formset_kwargs', {}) + kwargs['formset_kwargs']['instance'] = package + + if request.method == 'POST': + formset = kwargs['formset_factory'](data=request.POST, **kwargs['formset_kwargs']) + if formset.is_valid(): + formset.save() + return create_update.redirect(kwargs.get('post_save_redirect', None), + package) + + formset = kwargs['formset_factory'](**kwargs['formset_kwargs']) + + kwargs['extra_context']['formset'] = formset + + return render_to_response(kwargs['template_name'], kwargs['extra_context'], + context_instance=RequestContext(request), + mimetype=kwargs['mimetype']) diff --git a/userpypi/views/releases.py b/userpypi/views/releases.py new file mode 100644 index 0000000..9e15cd2 --- /dev/null +++ b/userpypi/views/releases.py @@ -0,0 +1,255 @@ +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from django.forms.models import inlineformset_factory +from django.http import Http404 +from django.views.generic import ListView, DetailView +from django.views.generic import create_update +from django.shortcuts import get_object_or_404, render_to_response +from django.template import RequestContext + +from userpypi.decorators import user_maintains_package +from userpypi.models import Package, Release, Distribution +from userpypi.forms import ReleaseForm, DistributionUploadForm +from userpypi.settings import METADATA_FORMS +from userpypi.utils import get_class + +class ReleaseOwnerObjectMixin(object): + def get_context_data(self, **kwargs): + context = super(ReleaseOwnerObjectMixin, self).get_context_data(**kwargs) + context['owner'] = self.get_owner() + context['is_owner'] = self.owner == self.request.user.username + return context + + def get_owner(self): + """ + Set the owner user object + """ + owner = getattr(self, 'owner') + if owner and issubclass(owner.__class__, User): + return owner + owner = self.kwargs.get('owner', None) + if owner is not None: + try: + self.owner = User.objects.get(username=owner) + except User.DoesNotExist: + raise Http404 + else: + raise Http404 + return self.owner + + def get_queryset(self): + """ + Filter the queryset based on whether or not the requesting user is + the owner of the requested objects + """ + if self.request.user != self.get_owner(): + if self.owner.profile.organization: + params = dict(package__owner__username=self.owner) + else: + params = dict(package__owner=self.request.user, private=False) + return self.model.objects.filter(**params) + + +class ReleaseListView(ReleaseOwnerObjectMixin, ListView): + model = Release + context_object_name = 'release_list' + simple = False + owner = None + + def get_template_names(self): + """ + Returns a list of template names to be used for the request. Must return + a list. May not be called if render_to_response is overridden. + """ + if self.simple: + return ['userpypi/release_list_simple.html'] + else: + return ['userpypi/release_list.html'] + + +class ReleaseDetailView(ReleaseOwnerObjectMixin, DetailView): + model = Release + context_object_name = 'release' + doap = False + owner = None + + def render_to_response(self, context, **response_kwargs): + """ + Returns a response with a template rendered with the given context. + """ + if self.doap: + response_kwargs['mimetype'] = 'text/xml' + + return super(ReleaseDetailView, self).render_to_response(context, **response_kwargs) + + def get_object(self): + package = self.kwargs.get('package', None) + try: + queryset = self.get_queryset().filter(package__name=package) + obj = queryset.get() + except ObjectDoesNotExist: + raise Http404(_(u"No %(verbose_name)s found matching the query") % + {'verbose_name': queryset.model._meta.verbose_name}) + return obj + + def get_template_names(self): + """ + Returns a list of template names to be used for the request. Must return + a list. May not be called if render_to_response is overridden. + """ + self.doap = 'doap' in self.kwargs and self.kwargs['doap'] + + if self.doap: + return ['userpypi/release_doap.xml'] + else: + return ['userpypi/release_detail.html'] + + +@user_maintains_package() +def manage(request, owner, package, version, **kwargs): + kwargs.pop('owner') + release = get_object_or_404(Package, owner=request.user, name=package).get_release(version) + + if not release: + raise Http404('Version %s does not exist for %s' % (version, + package,)) + + kwargs['object_id'] = release.pk + + kwargs.setdefault('form_class', ReleaseForm) + kwargs.setdefault('template_name', 'userpypi/release_manage.html') + kwargs.setdefault('template_object_name', 'release') + + return create_update.update_object(request, **kwargs) + +@user_maintains_package() +def manage_metadata(request, owner, package, version, **kwargs): + kwargs.pop('owner') + kwargs.setdefault('template_name', 'userpypi/release_manage.html') + kwargs.setdefault('template_object_name', 'release') + kwargs.setdefault('extra_context', {}) + kwargs.setdefault('mimetype', settings.DEFAULT_CONTENT_TYPE) + + release = get_object_or_404(Package, owner=request.user, name=package).get_release(version) + + if not release: + raise Http404('Version %s does not exist for %s' % (version, + package,)) + + if not release.metadata_version in METADATA_FORMS: + #TODO: Need to change this to a more meaningful error + raise Http404() + + kwargs['extra_context'][kwargs['template_object_name']] = release + + form_class = get_class(METADATA_FORMS.get(release.metadata_version)) + + initial = {} + multivalue = ('classifier',) + + for key, values in release.package_info.iterlists(): + if key in multivalue: + initial[key] = values + else: + initial[key] = '\n'.join(values) + + if request.method == 'POST': + form = form_class(data=request.POST, initial=initial) + + if form.is_valid(): + for key, value in form.cleaned_data.iteritems(): + if isinstance(value, basestring): + release.package_info[key] = value + elif hasattr(value, '__iter__'): + release.package_info.setlist(key, list(value)) + + release.save() + return create_update.redirect(kwargs.get('post_save_redirect',None), + release) + else: + form = form_class(initial=initial) + + kwargs['extra_context']['form'] = form + + return render_to_response(kwargs['template_name'], kwargs['extra_context'], + context_instance=RequestContext(request), + mimetype=kwargs['mimetype']) + +@user_maintains_package() +def manage_files(request, package, version, **kwargs): + release = get_object_or_404(Package, owner=request.user, name=package).get_release(version) + + if not release: + raise Http404('Version %s does not exist for %s' % (version, + package,)) + + kwargs.setdefault('formset_factory_kwargs',{}) + kwargs['formset_factory_kwargs'].setdefault('fields', ('comment',)) + kwargs['formset_factory_kwargs']['extra'] = 0 + + kwargs.setdefault('formset_factory', inlineformset_factory(Release, Distribution, **kwargs['formset_factory_kwargs'])) + kwargs.setdefault('template_name', 'userpypi/release_manage_files.html') + kwargs.setdefault('template_object_name', 'release') + kwargs.setdefault('extra_context',{}) + kwargs.setdefault('mimetype',settings.DEFAULT_CONTENT_TYPE) + kwargs['extra_context'][kwargs['template_object_name']] = release + kwargs.setdefault('formset_kwargs',{}) + kwargs['formset_kwargs']['instance'] = release + kwargs.setdefault('upload_form_factory', DistributionUploadForm) + + if request.method == 'POST': + formset = kwargs['formset_factory'](data=request.POST, + files=request.FILES, + **kwargs['formset_kwargs']) + if formset.is_valid(): + formset.save() + formset = kwargs['formset_factory'](**kwargs['formset_kwargs']) + else: + formset = kwargs['formset_factory'](**kwargs['formset_kwargs']) + + kwargs['extra_context']['formset'] = formset + kwargs['extra_context'].setdefault('upload_form', + kwargs['upload_form_factory']()) + + return render_to_response(kwargs['template_name'], kwargs['extra_context'], + context_instance=RequestContext(request), + mimetype=kwargs['mimetype']) + +@user_maintains_package() +def upload_file(request, package, version, **kwargs): + release = get_object_or_404(Package, owner=request.user, name=package).get_release(version) + + if not release: + raise Http404('Version %s does not exist for %s' % (version, + package,)) + + kwargs.setdefault('form_factory', DistributionUploadForm) + kwargs.setdefault('post_save_redirect', reverse('userpypi-release-manage-files', + kwargs={'package': package, + 'version': version})) + kwargs.setdefault('template_name', 'userpypi/release_upload_file.html') + kwargs.setdefault('template_object_name', 'release') + kwargs.setdefault('extra_context',{}) + kwargs.setdefault('mimetype',settings.DEFAULT_CONTENT_TYPE) + kwargs['extra_context'][kwargs['template_object_name']] = release + + if request.method == 'POST': + form = kwargs['form_factory'](data=request.POST, files=request.FILES) + if form.is_valid(): + dist = form.save(commit=False) + dist.release = release + dist.uploader = request.user + dist.save() + + return create_update.redirect(kwargs.get('post_save_redirect'), + release) + else: + form = kwargs['form_factory']() + + kwargs['extra_context']['form'] = form + + return render_to_response(kwargs['template_name'], kwargs['extra_context'], + context_instance=RequestContext(request), + mimetype=kwargs['mimetype']) diff --git a/userpypi/views/xmlrpc.py b/userpypi/views/xmlrpc.py new file mode 100644 index 0000000..077e119 --- /dev/null +++ b/userpypi/views/xmlrpc.py @@ -0,0 +1,139 @@ +import xmlrpclib + +from django.http import HttpResponseNotAllowed, HttpResponse + +from userpypi.models import Package, Release +from userpypi.settings import XMLRPC_COMMANDS +from userpypi.utils import get_class + +class XMLRPCResponse(HttpResponse): + """ A wrapper around the base HttpResponse that dumps the output for xmlrpc + use """ + def __init__(self, params=(), methodresponse=True, *args, **kwargs): + super(XMLRPCResponse, self).__init__(xmlrpclib.dumps(params, + methodresponse=methodresponse), + *args, **kwargs) + +def parse_xmlrpc_request(request): + """ + Parse the request and dispatch to the appropriate view + """ + args, command = xmlrpclib.loads(request.raw_post_data) + + if command in XMLRPC_COMMANDS: + return get_class(XMLRPC_COMMANDS[command])(request, *args) + else: + return HttpResponseNotAllowed(XMLRPC_COMMANDS.keys()) + +def list_packages(request): + return XMLRPCResponse(params=(list(Package.objects.all().values_list('name', flat=True)),), + content_type='text/xml') + +def package_releases(request, package_name, show_hidden=False): + try: + return XMLRPCResponse(params=(list(Package.objects.get(name=package_name).releases.filter(hidden=show_hidden).values_list('version', flat=True)),)) + except Package.DoesNotExist: + return XMLRPCResponse(params=([],)) + +def release_urls(request, package_name, version): + base_url = '%s://%s' % (request.is_secure() and 'https' or 'http', + request.get_host()) + dists = [] + try: + for dist in Package.objects.get(name=package_name).releases.get(version=version).distributions.all(): + dists.append({ + 'url': '%s%s' % (base_url, dist.get_absolute_url()), + 'packagetype': dist.filetype, + 'filename': dist.filename, + 'size': dist.content.size, + 'md5_digest': dist.md5_digest, + 'downloads': 0, + 'has_sig': len(dist.signature)>0, + 'python_version': dist.pyversion, + 'comment_text': dist.comment + }) + except (Package.DoesNotExist, Release.DoesNotExist): + pass + + return XMLRPCResponse(params=(dists,)) + +def release_data(request, package_name, version): + output = { + 'name': '', + 'version': '', + 'stable_version': '', + 'author': '', + 'author_email': '', + 'maintainer': '', + 'maintainer_email': '', + 'home_page': '', + 'license': '', + 'summary': '', + 'description': '', + 'keywords': '', + 'platform': '', + 'download_url': '', + 'classifiers': '', + 'requires': '', + 'requires_dist': '', + 'provides': '', + 'provides_dist': '', + 'requires_external': '', + 'requires_python': '', + 'obsoletes': '', + 'obsoletes_dist': '', + 'project_url': '', + } + try: + release = Package.objects.get(name=package_name).releases.get(version=version) + output.update({'name': package_name, 'version': version,}) + output.update(release.package_info) + except (Package.DoesNotExist, Release.DoesNotExist): + pass + + return XMLRPCResponse(params=(output,)) + +def search(request, spec, operator='or'): + """ + search(spec[, operator]) + + Search the package database using the indicated search spec. + The spec may include any of the keywords described in the above list (except 'stable_version' and 'classifiers'), for example: {'description': 'spam'} will search description fields. Within the spec, a field's value can be a string or a list of strings (the values within the list are combined with an OR), for example: {'name': ['foo', 'bar']}. Valid keys for the spec dict are listed here. Invalid keys are ignored: + name + version + author + author_email + maintainer + maintainer_email + home_page + license + summary + description + keywords + platform + download_url + Arguments for different fields are combined using either "and" (the default) or "or". Example: search({'name': 'foo', 'description': 'bar'}, 'or'). The results are returned as a list of dicts {'name': package name, 'version': package release version, 'summary': package release summary} + + changelog(since) + + Retrieve a list of four-tuples (name, version, timestamp, action) since the given timestamp. All timestamps are UTC values. The argument is a UTC integer seconds since the epoch. + """ + + output = { + 'name': '', + 'version': '', + 'summary': '', + } + return XMLRPCResponse(params=(output,)) + +def changelog(since): + output = { + 'name': '', + 'version': '', + 'timestamp': '', + 'action': '', + } + return XMLRPCResponse(params=(output,)) + +def ratings(request, name, version, since): + return XMLRPCResponse(params=([],))