diff options
author | Ask Solem <askh@opera.com> | 2009-04-21 15:35:13 +0200 |
---|---|---|
committer | Ask Solem <askh@opera.com> | 2009-04-21 15:35:13 +0200 |
commit | 647731a40471cfbaf4805a345f1db9ac88cf6343 (patch) | |
tree | beef005161c1534c24ca9c09ba2ed018697d98ba | |
parent | ed4f4a59a0cff99411a5a48dae90faf327e6da92 (diff) | |
parent | 81344edb1d615dc93faaf4b5581aa8277087efa6 (diff) | |
download | chishop-647731a40471cfbaf4805a345f1db9ac88cf6343.tar.bz2 chishop-647731a40471cfbaf4805a345f1db9ac88cf6343.tar.xz chishop-647731a40471cfbaf4805a345f1db9ac88cf6343.zip |
Merge branch 'russell/master'
-rw-r--r-- | AUTHORS | 2 | ||||
-rw-r--r-- | README | 40 | ||||
-rw-r--r-- | TODO | 4 | ||||
-rw-r--r-- | chishop/settings.py | 1 | ||||
-rw-r--r-- | djangopypi/forms.py | 85 | ||||
-rw-r--r-- | djangopypi/models.py | 5 | ||||
-rw-r--r-- | djangopypi/views.py | 104 |
7 files changed, 134 insertions, 107 deletions
@@ -1,3 +1,3 @@ | |||
1 | Ask Solem <askh@opera.com> | 1 | Ask Solem <askh@opera.com> |
2 | Rune Halvorsen <runeh@opera.com> | 2 | Rune Halvorsen <runeh@opera.com> |
3 | Russel Sim <russel.sim@jcu.edu.au> | 3 | Russell Sim <russell.sim@gmail.com> |
@@ -15,6 +15,8 @@ First you have to install the dependencies:: | |||
15 | 15 | ||
16 | Initial configuration | 16 | Initial 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 | ||
24 | Run the PyPI server | 26 | Run 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 | |||
30 | Please note that ``chishop/media/dists`` has to be writable by the | 33 | Please note that ``chishop/media/dists`` has to be writable by the |
31 | user the web-server is running as. | 34 | user the web-server is running as. |
32 | 35 | ||
33 | Contact Information | 36 | Using Setuptools |
34 | ==================== | 37 | ================ |
35 | askh@opera.com | 38 | |
39 | Add 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 | |||
58 | Pushing a package to local PyPI | ||
59 | ----------------------------------- | ||
60 | |||
61 | 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. | ||
62 | |||
63 | To push the package to the local pypi:: | ||
64 | |||
65 | $ python setup.py mregister sdist mupload -r local | ||
66 | |||
67 | If 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 |
@@ -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. |
13 | DJANGOPYPI_ALLOW_VERSION_OVERWRITE = False | 13 | DJANGOPYPI_ALLOW_VERSION_OVERWRITE = False |
14 | DJANGOPYPI_RELEASE_UPLOAD_TO = 'dists' | ||
14 | 15 | ||
15 | MANAGERS = ADMINS | 16 | MANAGERS = 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 | |||
37 | from django.utils.translation import ugettext_lazy as _ | 37 | from django.utils.translation import ugettext_lazy as _ |
38 | 38 | ||
39 | 39 | ||
40 | class PermissionDeniedError(Exception): | 40 | class 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 | ||
44 | class AlreadyExistsError(Exception): | 46 | class ReleaseForm(forms.ModelForm): |
45 | """Filename already exists.""" | 47 | class Meta: |
48 | model = Release | ||
49 | exclude = ['project'] | ||
46 | 50 | ||
47 | ALREADY_EXISTS_FMT = _("""A file named "%s" already exists for %s. To fix """ | ||
48 | + "problems with that you should create a new release.") | ||
49 | |||
50 | |||
51 | class 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 | ||
33 | import os | 33 | import os |
34 | from django.conf import settings | ||
34 | from django.db import models | 35 | from django.db import models |
35 | from django.utils.translation import ugettext_lazy as _ | 36 | from django.utils.translation import ugettext_lazy as _ |
36 | from django.contrib.auth.models import User | 37 | from django.contrib.auth.models import User |
@@ -63,6 +64,8 @@ ARCHITECTURES = ( | |||
63 | ("ultrasparc", "UltraSparc"), | 64 | ("ultrasparc", "UltraSparc"), |
64 | ) | 65 | ) |
65 | 66 | ||
67 | UPLOAD_TO = getattr(settings, | ||
68 | "DJANGOPYPI_RELEASE_UPLOAD_TO", 'dist') | ||
66 | 69 | ||
67 | class Classifier(models.Model): | 70 | class 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 | ||
108 | class Release(models.Model): | 111 | class 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 | ||
33 | import os | ||
34 | |||
35 | from django.conf import settings | ||
33 | from django.http import Http404, HttpResponse, HttpResponseBadRequest | 36 | from django.http import Http404, HttpResponse, HttpResponseBadRequest |
34 | from django.http import QueryDict, HttpResponseForbidden | 37 | from django.http import QueryDict, HttpResponseForbidden |
35 | from django.shortcuts import render_to_response | 38 | from django.shortcuts import render_to_response |
36 | from djangopypi.models import Project | 39 | from djangopypi.models import Project, Classifier, Release, UPLOAD_TO |
37 | from djangopypi.forms import ProjectRegisterForm | 40 | from djangopypi.forms import ProjectForm, ReleaseForm |
38 | from django.template import RequestContext | 41 | from django.template import RequestContext |
39 | from django.utils.datastructures import MultiValueDict | 42 | from django.utils.datastructures import MultiValueDict |
43 | from django.utils.translation import ugettext_lazy as _ | ||
40 | from django.core.files.uploadedfile import SimpleUploadedFile | 44 | from django.core.files.uploadedfile import SimpleUploadedFile |
41 | from django.contrib.auth import authenticate, login | 45 | from django.contrib.auth import authenticate, login |
42 | from djangopypi.http import HttpResponseNotImplemented | 46 | from djangopypi.http import HttpResponseNotImplemented |
43 | from djangopypi.http import HttpResponseUnauthorized | 47 | from djangopypi.http import HttpResponseUnauthorized |
44 | 48 | ||
45 | 49 | ||
50 | ALREADY_EXISTS_FMT = _("""A file named "%s" already exists for %s. To fix """ | ||
51 | + "problems with that you should create a new release.") | ||
52 | |||
53 | |||
46 | def parse_weird_post_data(raw_post_data): | 54 | def 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 | ||
89 | def login_basic_auth(request): | 97 | def 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 | ||
109 | def 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 | |||
101 | def simple(request, template_name="djangopypi/simple.html"): | 161 | def 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, { |