aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAsk Solem <askh@opera.com>2009-04-21 15:35:13 +0200
committerAsk Solem <askh@opera.com>2009-04-21 15:35:13 +0200
commit647731a40471cfbaf4805a345f1db9ac88cf6343 (patch)
treebeef005161c1534c24ca9c09ba2ed018697d98ba
parented4f4a59a0cff99411a5a48dae90faf327e6da92 (diff)
parent81344edb1d615dc93faaf4b5581aa8277087efa6 (diff)
downloadchishop-647731a40471cfbaf4805a345f1db9ac88cf6343.tar.bz2
chishop-647731a40471cfbaf4805a345f1db9ac88cf6343.tar.xz
chishop-647731a40471cfbaf4805a345f1db9ac88cf6343.zip
Merge branch 'russell/master'
-rw-r--r--AUTHORS2
-rw-r--r--README40
-rw-r--r--TODO4
-rw-r--r--chishop/settings.py1
-rw-r--r--djangopypi/forms.py85
-rw-r--r--djangopypi/models.py5
-rw-r--r--djangopypi/views.py104
7 files changed, 134 insertions, 107 deletions
diff --git a/AUTHORS b/AUTHORS
index 915d906..71b6d81 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,3 +1,3 @@
1Ask Solem <askh@opera.com> 1Ask Solem <askh@opera.com>
2Rune Halvorsen <runeh@opera.com> 2Rune Halvorsen <runeh@opera.com>
3Russel Sim <russel.sim@jcu.edu.au> 3Russell Sim <russell.sim@gmail.com>
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::
15 15
16Initial configuration 16Initial configuration
17--------------------- 17---------------------
18::
19
18 $ cd chipshop/ 20 $ cd chipshop/
19 21
20 $ $EDITOR settings.py 22 $ $EDITOR settings.py
@@ -23,6 +25,7 @@ Initial configuration
23 25
24Run the PyPI server 26Run the PyPI server
25------------------- 27-------------------
28::
26 29
27 $ python manage.py runserver 30 $ python manage.py runserver
28 31
@@ -30,8 +33,39 @@ Run the PyPI server
30Please note that ``chishop/media/dists`` has to be writable by the 33Please note that ``chishop/media/dists`` has to be writable by the
31user the web-server is running as. 34user the web-server is running as.
32 35
33Contact Information 36Using Setuptools
34==================== 37================
35askh@opera.com 38
39Add the following to your ``~/.pypirc`` file::
40
41 [distutils]
42 index-servers =
43 pypi
44 local
45
46
47 [pypi]
48 username:user
49 password:secret
50
51 [local]
52
53 username:user
54 password:secret
55
56 repository:http://localhost:8000
57
58Pushing a package to local PyPI
59-----------------------------------
60
61instead 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.
62
63To push the package to the local pypi::
64
65 $ python setup.py mregister sdist mupload -r local
66
67If you don't have Python 2.6 please run the command below to install the backport of the extension::
68
69 $ easy_install -U collective.dist
36 70
37.. # vim: syntax=rst expandtab tabstop=4 shiftwidth=4 shiftround 71.. # vim: syntax=rst expandtab tabstop=4 shiftwidth=4 shiftround
diff --git a/TODO b/TODO
index 9cef3d3..bdd7d61 100644
--- a/TODO
+++ b/TODO
@@ -7,3 +7,7 @@
7* Maybe add a permission "can upload new release", so more than one 7* Maybe add a permission "can upload new release", so more than one
8 user can change the same project. 8 user can change the same project.
9* Should a project have co-owners? 9* Should a project have co-owners?
10 - One possible solution:
11 http://github.com/initcrash/django-object-permissions/tree
12* Script to populate classifiers from
13 http://pypi.python.org/pypi?%3Aaction=list_classifiers
diff --git a/chishop/settings.py b/chishop/settings.py
index 2bc2f20..e7c2121 100644
--- a/chishop/settings.py
+++ b/chishop/settings.py
@@ -11,6 +11,7 @@ ADMINS = (
11# The default on PyPI is to not allow this, but it can be real handy 11# The default on PyPI is to not allow this, but it can be real handy
12# if you're sloppy. 12# if you're sloppy.
13DJANGOPYPI_ALLOW_VERSION_OVERWRITE = False 13DJANGOPYPI_ALLOW_VERSION_OVERWRITE = False
14DJANGOPYPI_RELEASE_UPLOAD_TO = 'dists'
14 15
15MANAGERS = ADMINS 16MANAGERS = ADMINS
16 17
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
37from django.utils.translation import ugettext_lazy as _ 37from django.utils.translation import ugettext_lazy as _
38 38
39 39
40class PermissionDeniedError(Exception): 40class ProjectForm(forms.ModelForm):
41 """The user did not have the privileges to execute an action.""" 41 class Meta:
42 model = Project
43 exclude = ['owner', 'classifiers']
42 44
43 45
44class AlreadyExistsError(Exception): 46class ReleaseForm(forms.ModelForm):
45 """Filename already exists.""" 47 class Meta:
48 model = Release
49 exclude = ['project']
46 50
47ALREADY_EXISTS_FMT = _("""A file named "%s" already exists for %s. To fix """
48 + "problems with that you should create a new release.")
49
50
51class ProjectRegisterForm(forms.Form):
52 name = forms.CharField()
53 license = forms.CharField(required=False)
54 metadata_version = forms.CharField(initial="1.0")
55 author = forms.CharField(required=False)
56 home_page = forms.CharField(required=False)
57 download_url = forms.CharField(required=False)
58 summary = forms.CharField(required=False)
59 description = forms.CharField(required=False)
60 author_email = forms.CharField(required=False)
61 version = forms.CharField()
62 platform = forms.CharField(required=False)
63
64 PermissionDeniedError = PermissionDeniedError
65 AlreadyExistsError = AlreadyExistsError
66
67 def save(self, classifiers, user, file=None):
68 values = dict(self.cleaned_data)
69 name = values["name"]
70 version = values.pop("version")
71 platform = values.pop("platform", "UNKNOWN")
72 values["owner"] = user
73
74 try:
75 project = Project.objects.get(name=name)
76 except Project.DoesNotExist:
77 project = Project.objects.create(**values)
78 else:
79 # If the project already exists,
80 # be sure that the current user owns this object.
81 if project.owner != user:
82 raise self.PermissionDeniedError(
83 "%s doesn't own that project." % user.username)
84 [setattr(project, field_name, field_value)
85 for field_name, field_value in values.items()]
86 project.save()
87
88
89 for classifier in classifiers:
90 project.classifiers.add(
91 Classifier.objects.get_or_create(name=classifier)[0])
92
93 # If the old file already exists, django will append a _ after the
94 # filename, however with .tar.gz files django does the "wrong" thing
95 # and saves it as project-0.1.2.tar_.gz. So remove it before
96 # django sees anything.
97 allow_overwrite = getattr(settings,
98 "DJANGOPYPI_ALLOW_VERSION_OVERWRITE", False)
99
100 if file:
101 try:
102 release = Release.objects.get(version=version,
103 platform=platform, project=project)
104 if os.path.exists(release.distribution.path):
105 if not allow_overwrite:
106 raise self.AlreadyExistsError(ALREADY_EXISTS_FMT % (
107 release.filename, release))
108 os.remove(release.distribution.path)
109
110 release.delete()
111 except (Release.DoesNotExist, ValueError):
112 pass
113
114 release, created = Release.objects.get_or_create(version=version,
115 platform=platform,
116 project=project)
117 if file:
118 release.distribution.save(file.name, file, save=True)
119 release.save()
diff --git a/djangopypi/models.py b/djangopypi/models.py
index 6182d45..4821237 100644
--- a/djangopypi/models.py
+++ b/djangopypi/models.py
@@ -31,6 +31,7 @@ POSSIBILITY OF SUCH DAMAGE.
31""" 31"""
32 32
33import os 33import os
34from django.conf import settings
34from django.db import models 35from django.db import models
35from django.utils.translation import ugettext_lazy as _ 36from django.utils.translation import ugettext_lazy as _
36from django.contrib.auth.models import User 37from django.contrib.auth.models import User
@@ -63,6 +64,8 @@ ARCHITECTURES = (
63 ("ultrasparc", "UltraSparc"), 64 ("ultrasparc", "UltraSparc"),
64) 65)
65 66
67UPLOAD_TO = getattr(settings,
68 "DJANGOPYPI_RELEASE_UPLOAD_TO", 'dist')
66 69
67class Classifier(models.Model): 70class Classifier(models.Model):
68 name = models.CharField(max_length=255, unique=True) 71 name = models.CharField(max_length=255, unique=True)
@@ -107,7 +110,7 @@ class Project(models.Model):
107 110
108class Release(models.Model): 111class Release(models.Model):
109 version = models.CharField(max_length=128) 112 version = models.CharField(max_length=128)
110 distribution = models.FileField(upload_to="dists") 113 distribution = models.FileField(upload_to=UPLOAD_TO)
111 md5_digest = models.CharField(max_length=255, blank=True) 114 md5_digest = models.CharField(max_length=255, blank=True)
112 platform = models.CharField(max_length=255, blank=True) 115 platform = models.CharField(max_length=255, blank=True)
113 signature = models.CharField(max_length=128, blank=True) 116 signature = models.CharField(max_length=128, blank=True)
diff --git a/djangopypi/views.py b/djangopypi/views.py
index 5ed27d4..86fdd0d 100644
--- a/djangopypi/views.py
+++ b/djangopypi/views.py
@@ -30,19 +30,27 @@ POSSIBILITY OF SUCH DAMAGE.
30 30
31""" 31"""
32 32
33import os
34
35from django.conf import settings
33from django.http import Http404, HttpResponse, HttpResponseBadRequest 36from django.http import Http404, HttpResponse, HttpResponseBadRequest
34from django.http import QueryDict, HttpResponseForbidden 37from django.http import QueryDict, HttpResponseForbidden
35from django.shortcuts import render_to_response 38from django.shortcuts import render_to_response
36from djangopypi.models import Project 39from djangopypi.models import Project, Classifier, Release, UPLOAD_TO
37from djangopypi.forms import ProjectRegisterForm 40from djangopypi.forms import ProjectForm, ReleaseForm
38from django.template import RequestContext 41from django.template import RequestContext
39from django.utils.datastructures import MultiValueDict 42from django.utils.datastructures import MultiValueDict
43from django.utils.translation import ugettext_lazy as _
40from django.core.files.uploadedfile import SimpleUploadedFile 44from django.core.files.uploadedfile import SimpleUploadedFile
41from django.contrib.auth import authenticate, login 45from django.contrib.auth import authenticate, login
42from djangopypi.http import HttpResponseNotImplemented 46from djangopypi.http import HttpResponseNotImplemented
43from djangopypi.http import HttpResponseUnauthorized 47from djangopypi.http import HttpResponseUnauthorized
44 48
45 49
50ALREADY_EXISTS_FMT = _("""A file named "%s" already exists for %s. To fix """
51 + "problems with that you should create a new release.")
52
53
46def parse_weird_post_data(raw_post_data): 54def parse_weird_post_data(raw_post_data):
47 """ For some reason Django can't parse the HTTP POST data 55 """ For some reason Django can't parse the HTTP POST data
48 sent by ``distutils`` register/upload commands. 56 sent by ``distutils`` register/upload commands.
@@ -72,18 +80,18 @@ def parse_weird_post_data(raw_post_data):
72 if "filename" in headers: 80 if "filename" in headers:
73 file = SimpleUploadedFile(headers["filename"], content, 81 file = SimpleUploadedFile(headers["filename"], content,
74 content_type="application/gzip") 82 content_type="application/gzip")
75 files[headers["name"]] = file 83 files["distribution"] = [file]
76 elif headers["name"] in post_data: 84 elif headers["name"] in post_data:
77 post_data[headers["name"]].append(content) 85 post_data[headers["name"]].append(content)
78 else: 86 else:
79 # Distutils sends UNKNOWN for empty fields (e.g platform) 87 # Distutils sends UNKNOWN for empty fields (e.g platform)
80 # [russel.sim@jcu.edu.au] 88 # [russell.sim@gmail.com]
81 if content == 'UNKNOWN': 89 if content == 'UNKNOWN':
82 post_data[headers["name"]] = [None] 90 post_data[headers["name"]] = [None]
83 else: 91 else:
84 post_data[headers["name"]] = [content] 92 post_data[headers["name"]] = [content]
85 93
86 return MultiValueDict(post_data), files 94 return MultiValueDict(post_data), MultiValueDict(files)
87 95
88 96
89def login_basic_auth(request): 97def login_basic_auth(request):
@@ -98,30 +106,76 @@ def login_basic_auth(request):
98 return authenticate(username=username, password=password) 106 return authenticate(username=username, password=password)
99 107
100 108
109def submit_project_or_release(user, post_data, files):
110 """Registers/updates a project or release"""
111 try:
112 project = Project.objects.get(name=post_data['name'])
113 if project.owner != user:
114 return HttpResponseForbidden(
115 "That project is owned by someone else!")
116 except Project.DoesNotExist:
117 project = None
118
119 project_form = ProjectForm(post_data, instance=project)
120 if project_form.is_valid():
121 project = project_form.save(commit=False)
122 project.owner = user
123 project.save()
124 for c in post_data.getlist('classifiers'):
125 classifier, created = Classifier.objects.get_or_create(name=c)
126 project.classifiers.add(classifier)
127 if files:
128 allow_overwrite = getattr(settings,
129 "DJANGOPYPI_ALLOW_VERSION_OVERWRITE", False)
130 try:
131 release = Release.objects.get(version=post_data['version'],
132 project=project,
133 distribution=UPLOAD_TO + '/' +
134 files['distribution']._name)
135 if not allow_overwrite:
136 return HttpResponseForbidden(ALREADY_EXISTS_FMT % (
137 release.filename, release))
138 except Release.DoesNotExist:
139 release = None
140
141 # If the old file already exists, django will append a _ after the
142 # filename, however with .tar.gz files django does the "wrong"
143 # thing and saves it as project-0.1.2.tar_.gz. So remove it before
144 # django sees anything.
145 release_form = ReleaseForm(post_data, files, instance=release)
146 if release_form.is_valid():
147 if release and os.path.exists(release.distribution.path):
148 os.remove(release.distribution.path)
149 release = release_form.save(commit=False)
150 release.project = project
151 release.save()
152 else:
153 return HttpResponseBadRequest(
154 "ERRORS: %s" % release_form.errors)
155 else:
156 return HttpResponseBadRequest("ERRORS: %s" % project_form.errors)
157
158 return HttpResponse()
159
160
101def simple(request, template_name="djangopypi/simple.html"): 161def simple(request, template_name="djangopypi/simple.html"):
102 if request.method == "POST": 162 if request.method == "POST":
103 user = login_basic_auth(request)
104 if not user:
105 return HttpResponseUnauthorized('PyPI')
106 login(request, user)
107 if not request.user.is_authenticated():
108 return HttpResponseForbidden(
109 "Not logged in, or invalid username/password.")
110 post_data, files = parse_weird_post_data(request.raw_post_data) 163 post_data, files = parse_weird_post_data(request.raw_post_data)
111 action = post_data.get(":action") 164 action = post_data.get(":action")
112 classifiers = post_data.getlist("classifiers") 165 if action == 'file_upload':
113 register_form = ProjectRegisterForm(post_data.copy()) 166 user = login_basic_auth(request)
114 if register_form.is_valid(): 167 if not user:
115 try: 168 return HttpResponseUnauthorized('PyPI')
116 register_form.save(classifiers, request.user, 169
117 file=files.get("content")) 170 login(request, user)
118 except register_form.PermissionDeniedError, e: 171 if not request.user.is_authenticated():
119 return HttpResonseForbidden( 172 return HttpResponseForbidden(
120 "That project is owned by someone else!") 173 "Not logged in, or invalid username/password.")
121 except register_form.AlreadyExistsError, e: 174
122 return HttpResponseForbidden(e) 175 return submit_project_or_release(user, post_data, files)
123 return HttpResponse("Successfully registered.") 176
124 return HttpResponse("ERRORS: %s" % register_form.errors) 177 return HttpResponseNotImplemented(
178 "The :action %s is not implemented" % action)
125 179
126 dists = Project.objects.all().order_by("name") 180 dists = Project.objects.all().order_by("name")
127 context = RequestContext(request, { 181 context = RequestContext(request, {