From 9912d25f105a6a9ae0ead2a18f41140992a5ffef Mon Sep 17 00:00:00 2001 From: Russell Sim Date: Tue, 21 Apr 2009 17:55:27 +1000 Subject: [PATCH 1/7] changed my email address to my current work one. --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 915d906..31410d5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,3 +1,3 @@ Ask Solem Rune Halvorsen -Russel Sim +Russel Sim From 2f86492b02aaf5d1ccc8abc02d5cc711819ed8ef Mon Sep 17 00:00:00 2001 From: Ask Solem Date: Tue, 21 Apr 2009 11:30:16 +0200 Subject: [PATCH 2/7] Change Release.license to TextField (someone seems to be sending their whole LICENSE.txt file!) --- djangopypi/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangopypi/models.py b/djangopypi/models.py index d6df975..6182d45 100644 --- a/djangopypi/models.py +++ b/djangopypi/models.py @@ -77,7 +77,7 @@ class Classifier(models.Model): class Project(models.Model): name = models.CharField(max_length=255, unique=True) - license = models.CharField(max_length=255, blank=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) From 0acf3892c113e65cc5087edbe73149f6704df086 Mon Sep 17 00:00:00 2001 From: Russell Sim Date: Tue, 21 Apr 2009 20:51:23 +1000 Subject: [PATCH 3/7] cleaned up README, added simple usage example --- README | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/README b/README index b0d224d..08e0189 100644 --- a/README +++ b/README @@ -15,6 +15,8 @@ First you have to install the dependencies:: Initial configuration --------------------- +:: + $ cd chipshop/ $ $EDITOR settings.py @@ -23,6 +25,7 @@ Initial configuration Run the PyPI server ------------------- +:: $ python manage.py runserver @@ -30,8 +33,39 @@ Run the PyPI server Please note that ``chishop/media/dists`` has to be writable by the user the web-server is running as. -Contact Information -==================== -askh@opera.com +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 + +Pushing a package to local PyPI +----------------------------------- + +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 sdist mupload -r local + +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 .. # vim: syntax=rst expandtab tabstop=4 shiftwidth=4 shiftround From b9acbab26cd672b3fe441ae2e111ce575ae6b8ef Mon Sep 17 00:00:00 2001 From: Russell Sim Date: Tue, 21 Apr 2009 20:58:18 +1000 Subject: [PATCH 4/7] Whoops, don't even know my own email address... --- AUTHORS | 2 +- djangopypi/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 31410d5..71b6d81 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,3 +1,3 @@ Ask Solem Rune Halvorsen -Russel Sim +Russell Sim diff --git a/djangopypi/views.py b/djangopypi/views.py index 29494a7..54d58dc 100644 --- a/djangopypi/views.py +++ b/djangopypi/views.py @@ -79,7 +79,7 @@ def parse_weird_post_data(raw_post_data): post_data[headers["name"]].append(content) else: # Distutils sends UNKNOWN for empty fields (e.g platform) - # [russel.sim@jcu.edu.au] + # [russell.sim@gmail.com] if content == 'UNKNOWN': post_data[headers["name"]] = [None] else: From 05af358f9cbeeb04216f649ffec6c828edac794c Mon Sep 17 00:00:00 2001 From: Russell Sim Date: Tue, 21 Apr 2009 21:04:16 +1000 Subject: [PATCH 5/7] complete refactor to use ModelForm --- chishop/settings.py | 1 + djangopypi/forms.py | 85 ++++-------------------------------- djangopypi/models.py | 5 ++- djangopypi/views.py | 102 +++++++++++++++++++++++++++++++++---------- 4 files changed, 91 insertions(+), 102 deletions(-) diff --git a/chishop/settings.py b/chishop/settings.py index 9d769f2..22dff36 100644 --- a/chishop/settings.py +++ b/chishop/settings.py @@ -15,6 +15,7 @@ ADMINS = ( # The default on PyPI is to not allow this, but it can be real handy # if you're sloppy. DJANGOPYPI_ALLOW_VERSION_OVERWRITE = False +DJANGOPYPI_RELEASE_UPLOAD_TO = 'dists' MANAGERS = ADMINS diff --git a/djangopypi/forms.py b/djangopypi/forms.py index 51b1498..7704753 100644 --- a/djangopypi/forms.py +++ b/djangopypi/forms.py @@ -37,83 +37,14 @@ from djangopypi.models import Project, Classifier, Release from django.utils.translation import ugettext_lazy as _ -class PermissionDeniedError(Exception): - """The user did not have the privileges to execute an action.""" +class ProjectForm(forms.ModelForm): + class Meta: + model = Project + exclude = ['owner', 'classifiers'] -class AlreadyExistsError(Exception): - """Filename already exists.""" +class ReleaseForm(forms.ModelForm): + class Meta: + model = Release + exclude = ['project'] -ALREADY_EXISTS_FMT = _("""A file named "%s" already exists for %s. To fix """ - + "problems with that you should create a new release.") - - -class ProjectRegisterForm(forms.Form): - name = forms.CharField() - license = forms.CharField(required=False) - metadata_version = forms.CharField(initial="1.0") - author = forms.CharField(required=False) - home_page = forms.CharField(required=False) - download_url = forms.CharField(required=False) - summary = forms.CharField(required=False) - description = forms.CharField(required=False) - author_email = forms.CharField(required=False) - version = forms.CharField() - platform = forms.CharField(required=False) - - PermissionDeniedError = PermissionDeniedError - AlreadyExistsError = AlreadyExistsError - - def save(self, classifiers, user, file=None): - values = dict(self.cleaned_data) - name = values["name"] - version = values.pop("version") - platform = values.pop("platform", "UNKNOWN") - values["owner"] = user - - try: - project = Project.objects.get(name=name) - except Project.DoesNotExist: - project = Project.objects.create(**values) - else: - # If the project already exists, - # be sure that the current user owns this object. - if project.owner != user: - raise self.PermissionDeniedError( - "%s doesn't own that project." % user.username) - [setattr(project, field_name, field_value) - for field_name, field_value in values.items()] - project.save() - - - for classifier in classifiers: - project.classifiers.add( - Classifier.objects.get_or_create(name=classifier)[0]) - - # 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. - allow_overwrite = getattr(settings, - "DJANGOPYPI_ALLOW_VERSION_OVERWRITE", False) - - if file: - try: - release = Release.objects.get(version=version, - platform=platform, project=project) - if os.path.exists(release.distribution.path): - if not allow_overwrite: - raise self.AlreadyExistsError(ALREADY_EXISTS_FMT % ( - release.filename, release)) - os.remove(release.distribution.path) - - release.delete() - except (Release.DoesNotExist, ValueError): - pass - - release, created = Release.objects.get_or_create(version=version, - platform=platform, - project=project) - if file: - release.distribution.save(file.name, file, save=True) - release.save() diff --git a/djangopypi/models.py b/djangopypi/models.py index d6df975..b4b1e34 100644 --- a/djangopypi/models.py +++ b/djangopypi/models.py @@ -31,6 +31,7 @@ POSSIBILITY OF SUCH DAMAGE. """ 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 @@ -63,6 +64,8 @@ ARCHITECTURES = ( ("ultrasparc", "UltraSparc"), ) +UPLOAD_TO = getattr(settings, + "DJANGOPYPI_RELEASE_UPLOAD_TO", 'dist') class Classifier(models.Model): name = models.CharField(max_length=255, unique=True) @@ -107,7 +110,7 @@ class Project(models.Model): class Release(models.Model): version = models.CharField(max_length=128) - distribution = models.FileField(upload_to="dists") + distribution = models.FileField(upload_to=UPLOAD_TO) md5_digest = models.CharField(max_length=255, blank=True) platform = models.CharField(max_length=255, blank=True) signature = models.CharField(max_length=128, blank=True) diff --git a/djangopypi/views.py b/djangopypi/views.py index 54d58dc..3c6d03d 100644 --- a/djangopypi/views.py +++ b/djangopypi/views.py @@ -30,19 +30,27 @@ POSSIBILITY OF SUCH DAMAGE. """ +import os + +from django.conf import settings from django.http import Http404, HttpResponse, HttpResponseBadRequest from django.http import QueryDict, HttpResponseForbidden from django.shortcuts import render_to_response -from djangopypi.models import Project -from djangopypi.forms import ProjectRegisterForm +from djangopypi.models import Project, Classifier, Release, UPLOAD_TO +from djangopypi.forms import ProjectForm, ReleaseForm from django.template import RequestContext from django.utils.datastructures import MultiValueDict +from django.utils.translation import ugettext_lazy as _ from django.core.files.uploadedfile import SimpleUploadedFile from django.contrib.auth import authenticate, login from djangopypi.http import HttpResponseNotImplemented from djangopypi.http import HttpResponseUnauthorized +ALREADY_EXISTS_FMT = _("""A file named "%s" already exists for %s. To fix """ + + "problems with that you should create a new release.") + + def parse_weird_post_data(raw_post_data): """ For some reason Django can't parse the HTTP POST data sent by ``distutils`` register/upload commands. @@ -74,7 +82,7 @@ def parse_weird_post_data(raw_post_data): if "filename" in headers: file = SimpleUploadedFile(headers["filename"], content, content_type="application/gzip") - files[headers["name"]] = file + files["distribution"] = [file] elif headers["name"] in post_data: post_data[headers["name"]].append(content) else: @@ -85,7 +93,7 @@ def parse_weird_post_data(raw_post_data): else: post_data[headers["name"]] = [content] - return MultiValueDict(post_data), files + return MultiValueDict(post_data), MultiValueDict(files) def login_basic_auth(request): @@ -100,30 +108,76 @@ def login_basic_auth(request): return authenticate(username=username, password=password) +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 simple(request, template_name="djangopypi/simple.html"): if request.method == "POST": - 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.") post_data, files = parse_weird_post_data(request.raw_post_data) action = post_data.get(":action") - classifiers = post_data.getlist("classifiers") - register_form = ProjectRegisterForm(post_data.copy()) - if register_form.is_valid(): - try: - register_form.save(classifiers, request.user, - file=files.get("content")) - except register_form.PermissionDeniedError, e: - return HttpResonseForbidden( - "That project is owned by someone else!") - except register_form.AlreadyExistsError, e: - return HttpResponseForbidden(e) - return HttpResponse("Successfully registered.") - return HttpResponse("ERRORS: %s" % register_form.errors) + if action == 'file_upload': + 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) + + return HttpResponseNotImplemented( + "The :action %s is not implemented" % action) dists = Project.objects.all().order_by("name") context = RequestContext(request, { From 81344edb1d615dc93faaf4b5581aa8277087efa6 Mon Sep 17 00:00:00 2001 From: Russell Sim Date: Tue, 21 Apr 2009 21:45:52 +1000 Subject: [PATCH 6/7] more TODO tasks --- TODO | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TODO b/TODO index 9cef3d3..bdd7d61 100644 --- a/TODO +++ b/TODO @@ -7,3 +7,7 @@ * Maybe add a permission "can upload new release", so more than one user can change the same project. * Should a project have co-owners? + - One possible solution: + http://github.com/initcrash/django-object-permissions/tree +* Script to populate classifiers from + http://pypi.python.org/pypi?%3Aaction=list_classifiers From 39909f5d9e09622df9fd9e855831c9544b1de700 Mon Sep 17 00:00:00 2001 From: Russell Sim Date: Wed, 22 Apr 2009 08:36:32 +1000 Subject: [PATCH 7/7] updated documentation to make it clearer --- README | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/README b/README index 08e0189..bf6511b 100644 --- a/README +++ b/README @@ -52,11 +52,22 @@ Add the following to your ``~/.pypirc`` file:: username:user password:secret - repository:http://localhost:8000 -Pushing a package to local PyPI ------------------------------------ +Uploading a package: Python >=2.6 +-------------------------------------------- + +To push the package to the local pypi:: + + $ python setup.py register 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. @@ -64,8 +75,4 @@ To push the package to the local pypi:: $ python setup.py mregister sdist mupload -r local -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 - .. # vim: syntax=rst expandtab tabstop=4 shiftwidth=4 shiftround