aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBo Shi <bs1984@gmail.com>2009-12-22 23:27:32 -0500
committerBo Shi <bs1984@gmail.com>2009-12-22 23:27:32 -0500
commiteb834c564045a07e0014c5754231a857f13db663 (patch)
treee8d39787fb7b59a47620d13554658e936cd45308
parent93d46f74d7dec332670ed8671708d55d5134288a (diff)
parent1e2fdc99abff24259e1c47651ca34373a2dcf9d0 (diff)
downloadchishop-eb834c564045a07e0014c5754231a857f13db663.tar.bz2
chishop-eb834c564045a07e0014c5754231a857f13db663.tar.xz
chishop-eb834c564045a07e0014c5754231a857f13db663.zip
Merge branch 'master' of git://github.com/ask/chishop
-rw-r--r--.gitignore3
-rw-r--r--AUTHORS3
-rw-r--r--README7
-rw-r--r--TODO1
-rw-r--r--buildout.cfg2
-rw-r--r--chishop/conf/__init__.py0
-rw-r--r--chishop/conf/default.py111
-rw-r--r--chishop/development.py23
-rw-r--r--chishop/media/style/djangopypi.css4
-rw-r--r--chishop/production_example.py18
-rw-r--r--chishop/settings.py112
-rw-r--r--chishop/templates/base.html5
-rw-r--r--chishop/templates/djangopypi/search.html4
-rw-r--r--chishop/templates/djangopypi/search_results.html31
-rw-r--r--chishop/urls.py2
-rw-r--r--djangopypi/forms.py3
-rw-r--r--djangopypi/http.py51
-rw-r--r--djangopypi/tests.py22
-rw-r--r--djangopypi/urls.py5
-rw-r--r--djangopypi/views.py229
-rw-r--r--djangopypi/views/__init__.py76
-rw-r--r--djangopypi/views/dists.py79
-rw-r--r--djangopypi/views/search.py24
-rw-r--r--djangopypi/views/users.py24
24 files changed, 477 insertions, 362 deletions
diff --git a/.gitignore b/.gitignore
index d8e3b0d..b0a673f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
5*.sqlite-journal 5*.sqlite-journal
6settings_local.py 6settings_local.py
7.*.sw[po] 7.*.sw[po]
8*.kpf
8dist/ 9dist/
9*.egg-info 10*.egg-info
10doc/__build/* 11doc/__build/*
@@ -14,4 +15,4 @@ parts
14eggs 15eggs
15bin 16bin
16developer-eggs 17developer-eggs
17downloads \ No newline at end of file 18downloads
diff --git a/AUTHORS b/AUTHORS
index f2de5fc..3d4eeb3 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -4,3 +4,6 @@ Russell Sim <russell.sim@gmail.com>
4Brian Rosner <brosner@gmail.com> 4Brian Rosner <brosner@gmail.com>
5Hugo Lopes Tavares <hltbra@gmail.com> 5Hugo Lopes Tavares <hltbra@gmail.com>
6Sverre Johansen <sverre.johansen@gmail.com> 6Sverre Johansen <sverre.johansen@gmail.com>
7Bo Shi <bs@alum.mit.edu>
8Carl Meyer <carl@dirtcircle.com>
9Vinícius das Chagas Silva <vinimaster@gmail.com>
diff --git a/README b/README
index f497d1f..ca3b5e1 100644
--- a/README
+++ b/README
@@ -27,6 +27,13 @@ Run the PyPI server
27Please note that ``chishop/media/dists`` has to be writable by the 27Please note that ``chishop/media/dists`` has to be writable by the
28user the web-server is running as. 28user the web-server is running as.
29 29
30In production
31-------------
32
33You may want to copy the file ``chishop/production_example.py`` and modify
34for use as your production settings; you will also need to modify
35``bin/django.wsgi`` to refer to your production settings.
36
30Using Setuptools 37Using Setuptools
31================ 38================
32 39
diff --git a/TODO b/TODO
index 5d57eb7..f8ed064 100644
--- a/TODO
+++ b/TODO
@@ -11,7 +11,6 @@ PyPI feature replication
11 I'm not sure what the difference between a co-owner and maintainer is, 11 I'm not sure what the difference between a co-owner and maintainer is,
12 maybe it's just a label. 12 maybe it's just a label.
13* Package author admin interface (submit, edit, view) 13* Package author admin interface (submit, edit, view)
14* Search
15* Documentation upload 14* Documentation upload
16* Ratings 15* Ratings
17* Random Monty Python quotes :-) 16* Random Monty Python quotes :-)
diff --git a/buildout.cfg b/buildout.cfg
index 04e0acf..160db4c 100644
--- a/buildout.cfg
+++ b/buildout.cfg
@@ -8,7 +8,7 @@ eggs = pkginfo
8[django] 8[django]
9recipe = djangorecipe 9recipe = djangorecipe
10version = 1.1.1 10version = 1.1.1
11settings = development 11settings = settings
12eggs = ${buildout:eggs} 12eggs = ${buildout:eggs}
13test = djangopypi 13test = djangopypi
14project = chishop 14project = chishop
diff --git a/chishop/conf/__init__.py b/chishop/conf/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/chishop/conf/__init__.py
diff --git a/chishop/conf/default.py b/chishop/conf/default.py
new file mode 100644
index 0000000..97002b6
--- /dev/null
+++ b/chishop/conf/default.py
@@ -0,0 +1,111 @@
1# Django settings for djangopypi project.
2import os
3
4ADMINS = (
5 # ('Your Name', 'your_email@domain.com'),
6)
7
8# Allow uploading a new distribution file for a project version
9# if a file of that type already exists.
10#
11# The default on PyPI is to not allow this, but it can be real handy
12# if you're sloppy.
13DJANGOPYPI_ALLOW_VERSION_OVERWRITE = False
14DJANGOPYPI_RELEASE_UPLOAD_TO = 'dists'
15
16# change to False if you do not want Django's default server to serve static pages
17LOCAL_DEVELOPMENT = True
18
19REGISTRATION_OPEN = True
20ACCOUNT_ACTIVATION_DAYS = 7
21LOGIN_REDIRECT_URL = "/"
22
23EMAIL_HOST = ''
24DEFAULT_FROM_EMAIL = ''
25SERVER_EMAIL = DEFAULT_FROM_EMAIL
26
27MANAGERS = ADMINS
28
29DATABASE_ENGINE = ''
30DATABASE_NAME = ''
31DATABASE_USER = ''
32DATABASE_PASSWORD = ''
33DATABASE_HOST = ''
34DATABASE_PORT = ''
35
36# Local time zone for this installation. Choices can be found here:
37# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
38# although not all choices may be available on all operating systems.
39# If running in a Windows environment this must be set to the same as your
40# system time zone.
41TIME_ZONE = 'America/Chicago'
42
43# Language code for this installation. All choices can be found here:
44# http://www.i18nguy.com/unicode/language-identifiers.html
45LANGUAGE_CODE = 'en-us'
46
47SITE_ID = 1
48
49# If you set this to False, Django will make some optimizations so as not
50# to load the internationalization machinery.
51USE_I18N = True
52
53# Absolute path to the directory that holds media.
54# Example: "/home/media/media.lawrence.com/"
55here = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
56MEDIA_ROOT = os.path.join(here, 'media')
57
58# URL that handles the media served from MEDIA_ROOT. Make sure to use a
59# trailing slash if there is a path component (optional in other cases).
60# Examples: "http://media.lawrence.com", "http://example.com/media/"
61MEDIA_URL = '/media/'
62
63# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
64# trailing slash.
65# Examples: "http://foo.com/media/", "/media/".
66ADMIN_MEDIA_PREFIX = '/admin-media/'
67
68# Make this unique, and don't share it with anybody.
69SECRET_KEY = 'w_#0r2hh)=!zbynb*gg&969@)sy#^-^ia3m*+sd4@lst$zyaxu'
70
71# List of callables that know how to import templates from various sources.
72TEMPLATE_LOADERS = (
73 'django.template.loaders.filesystem.load_template_source',
74 'django.template.loaders.app_directories.load_template_source',
75# 'django.template.loaders.eggs.load_template_source',
76)
77
78MIDDLEWARE_CLASSES = (
79 'django.middleware.common.CommonMiddleware',
80 'django.contrib.sessions.middleware.SessionMiddleware',
81 'django.contrib.auth.middleware.AuthenticationMiddleware',
82)
83
84ROOT_URLCONF = 'urls'
85
86TEMPLATE_CONTEXT_PROCESSORS = (
87 "django.core.context_processors.auth",
88 "django.core.context_processors.debug",
89 "django.core.context_processors.i18n",
90 "django.core.context_processors.media",
91 "django.core.context_processors.request",
92)
93
94TEMPLATE_DIRS = (
95 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
96 # Always use forward slashes, even on Windows.
97 # Don't forget to use absolute paths, not relative paths.
98 os.path.join(os.path.dirname(os.path.dirname(__file__)), "templates"),
99)
100
101INSTALLED_APPS = (
102 'django.contrib.auth',
103 'django.contrib.contenttypes',
104 'django.contrib.sessions',
105 'django.contrib.sites',
106 'django.contrib.admin',
107 'django.contrib.markup',
108 'django.contrib.admindocs',
109 'registration',
110 'djangopypi',
111)
diff --git a/chishop/development.py b/chishop/development.py
deleted file mode 100644
index 0671fc2..0000000
--- a/chishop/development.py
+++ /dev/null
@@ -1,23 +0,0 @@
1from settings import *
2import os
3
4DEBUG = True
5TEMPLATE_DEBUG = DEBUG
6LOCAL_DEVELOPMENT = True
7
8if LOCAL_DEVELOPMENT:
9 import sys
10 sys.path.append(os.path.dirname(__file__))
11
12ADMINS = (
13 ('chishop', 'example@example.org'),
14)
15
16MANAGERS = ADMINS
17
18DATABASE_ENGINE = 'sqlite3'
19DATABASE_NAME = os.path.join(here, 'devdatabase.db')
20DATABASE_USER = ''
21DATABASE_PASSWORD = ''
22DATABASE_HOST = ''
23DATABASE_PORT = ''
diff --git a/chishop/media/style/djangopypi.css b/chishop/media/style/djangopypi.css
new file mode 100644
index 0000000..e6fbfd9
--- /dev/null
+++ b/chishop/media/style/djangopypi.css
@@ -0,0 +1,4 @@
1.search {
2 text-align:right;
3 margin-right: 10px;
4} \ No newline at end of file
diff --git a/chishop/production_example.py b/chishop/production_example.py
new file mode 100644
index 0000000..b64623e
--- /dev/null
+++ b/chishop/production_example.py
@@ -0,0 +1,18 @@
1from conf.default import *
2import os
3
4DEBUG = False
5TEMPLATE_DEBUG = DEBUG
6
7ADMINS = (
8 ('chishop', 'example@example.org'),
9)
10
11MANAGERS = ADMINS
12
13DATABASE_ENGINE = 'postgresql_psycopg2'
14DATABASE_NAME = 'chishop'
15DATABASE_USER = 'chishop'
16DATABASE_PASSWORD = 'chishop'
17DATABASE_HOST = ''
18DATABASE_PORT = ''
diff --git a/chishop/settings.py b/chishop/settings.py
index ee286b5..e68a4e5 100644
--- a/chishop/settings.py
+++ b/chishop/settings.py
@@ -1,113 +1,23 @@
1# Django settings for djangopypi project. 1from conf.default import *
2import os 2import os
3 3
4ADMINS = ( 4DEBUG = True
5 # ('Your Name', 'your_email@domain.com'), 5TEMPLATE_DEBUG = DEBUG
6)
7
8# Allow uploading a new distribution file for a project version
9# if a file of that type already exists.
10#
11# The default on PyPI is to not allow this, but it can be real handy
12# if you're sloppy.
13DJANGOPYPI_ALLOW_VERSION_OVERWRITE = False
14DJANGOPYPI_RELEASE_UPLOAD_TO = 'dists'
15
16# change to False if you do not want Django's default server to serve static pages
17LOCAL_DEVELOPMENT = True 6LOCAL_DEVELOPMENT = True
18 7
19REGISTRATION_OPEN = True 8if LOCAL_DEVELOPMENT:
20ACCOUNT_ACTIVATION_DAYS = 7 9 import sys
21LOGIN_REDIRECT_URL = "/" 10 sys.path.append(os.path.dirname(__file__))
22 11
23EMAIL_HOST = '' 12ADMINS = (
24DEFAULT_FROM_EMAIL = '' 13 ('chishop', 'example@example.org'),
25SERVER_EMAIL = DEFAULT_FROM_EMAIL 14)
26 15
27MANAGERS = ADMINS 16MANAGERS = ADMINS
28 17
29DATABASE_ENGINE = '' 18DATABASE_ENGINE = 'sqlite3'
30DATABASE_NAME = '' 19DATABASE_NAME = os.path.join(here, 'devdatabase.db')
31DATABASE_USER = '' 20DATABASE_USER = ''
32DATABASE_PASSWORD = '' 21DATABASE_PASSWORD = ''
33DATABASE_HOST = '' 22DATABASE_HOST = ''
34DATABASE_PORT = '' 23DATABASE_PORT = ''
35
36# Local time zone for this installation. Choices can be found here:
37# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
38# although not all choices may be available on all operating systems.
39# If running in a Windows environment this must be set to the same as your
40# system time zone.
41TIME_ZONE = 'America/Chicago'
42
43# Language code for this installation. All choices can be found here:
44# http://www.i18nguy.com/unicode/language-identifiers.html
45LANGUAGE_CODE = 'en-us'
46
47SITE_ID = 1
48
49# If you set this to False, Django will make some optimizations so as not
50# to load the internationalization machinery.
51USE_I18N = True
52
53# Absolute path to the directory that holds media.
54# Example: "/home/media/media.lawrence.com/"
55here = os.path.abspath(os.path.dirname(__file__))
56MEDIA_ROOT = os.path.join(here, 'media')
57
58# URL that handles the media served from MEDIA_ROOT. Make sure to use a
59# trailing slash if there is a path component (optional in other cases).
60# Examples: "http://media.lawrence.com", "http://example.com/media/"
61MEDIA_URL = 'media/'
62
63MEDIA_PREFIX = "/media/"
64
65# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
66# trailing slash.
67# Examples: "http://foo.com/media/", "/media/".
68ADMIN_MEDIA_PREFIX = '/admin-media/'
69
70# Make this unique, and don't share it with anybody.
71SECRET_KEY = 'w_#0r2hh)=!zbynb*gg&969@)sy#^-^ia3m*+sd4@lst$zyaxu'
72
73# List of callables that know how to import templates from various sources.
74TEMPLATE_LOADERS = (
75 'django.template.loaders.filesystem.load_template_source',
76 'django.template.loaders.app_directories.load_template_source',
77# 'django.template.loaders.eggs.load_template_source',
78)
79
80MIDDLEWARE_CLASSES = (
81 'django.middleware.common.CommonMiddleware',
82 'django.contrib.sessions.middleware.SessionMiddleware',
83 'django.contrib.auth.middleware.AuthenticationMiddleware',
84)
85
86ROOT_URLCONF = 'urls'
87
88TEMPLATE_CONTEXT_PROCESSORS = (
89 "django.core.context_processors.auth",
90 "django.core.context_processors.debug",
91 "django.core.context_processors.i18n",
92 "django.core.context_processors.media",
93 "django.core.context_processors.request",
94)
95
96TEMPLATE_DIRS = (
97 # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
98 # Always use forward slashes, even on Windows.
99 # Don't forget to use absolute paths, not relative paths.
100 os.path.join(os.path.dirname(__file__), "templates"),
101)
102
103INSTALLED_APPS = (
104 'django.contrib.auth',
105 'django.contrib.contenttypes',
106 'django.contrib.sessions',
107 'django.contrib.sites',
108 'django.contrib.admin',
109 'django.contrib.markup',
110 'django.contrib.admindocs',
111 'registration',
112 'djangopypi',
113)
diff --git a/chishop/templates/base.html b/chishop/templates/base.html
index 76483ce..dd797e7 100644
--- a/chishop/templates/base.html
+++ b/chishop/templates/base.html
@@ -2,6 +2,7 @@
2<html xmlns="http://www.w3.org/1999/xhtml" lang="en-au" xml:lang="en-au"> 2<html xmlns="http://www.w3.org/1999/xhtml" lang="en-au" xml:lang="en-au">
3<head> 3<head>
4<link rel="stylesheet" type="text/css" href="{% block stylesheet %}{% load adminmedia %}{% admin_media_prefix %}css/base.css{% endblock %}"/> 4<link rel="stylesheet" type="text/css" href="{% block stylesheet %}{% load adminmedia %}{% admin_media_prefix %}css/base.css{% endblock %}"/>
5<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}style/djangopypi.css"/>
5{% block extrastyle %}{% endblock %} 6{% block extrastyle %}{% endblock %}
6<meta http-equiv="content-type" content="text/html; charset=UTF-8"/> 7<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
7<title>{% block title %}{% endblock %}</title> 8<title>{% block title %}{% endblock %}</title>
@@ -20,6 +21,10 @@
20 {% block site_logo %}{% endblock %} 21 {% block site_logo %}{% endblock %}
21 <h1 id="site-name">{% block site_name_header %}{% endblock %}</h1> 22 <h1 id="site-name">{% block site_name_header %}{% endblock %}</h1>
22 </div> 23 </div>
24
25 <div class="search">
26 {% include "djangopypi/search.html" %}
27 </div>
23 28
24 <div id="user-tools"> 29 <div id="user-tools">
25 {% if user.is_authenticated %} 30 {% if user.is_authenticated %}
diff --git a/chishop/templates/djangopypi/search.html b/chishop/templates/djangopypi/search.html
new file mode 100644
index 0000000..8269508
--- /dev/null
+++ b/chishop/templates/djangopypi/search.html
@@ -0,0 +1,4 @@
1<form action='search' method='post'>
2 <input type="text" name="search_term" id="search_term">
3 <input type='submit' value=' Search '/>
4</form> \ No newline at end of file
diff --git a/chishop/templates/djangopypi/search_results.html b/chishop/templates/djangopypi/search_results.html
new file mode 100644
index 0000000..dccf61f
--- /dev/null
+++ b/chishop/templates/djangopypi/search_results.html
@@ -0,0 +1,31 @@
1{% extends "base_site.html" %}
2
3{% block bread_crumbs_1 %}&rsaquo;Search{% endblock %}
4
5{% block content %}
6 {% ifnotequal search_term ''%}
7 <h1>Index of Packages Matching '{{ search_term }}'</h1>
8 {% else %}
9 <h1>You need to supply a search term.</h1>
10 {% endifnotequal %}
11 {% if dists %}
12 <table>
13 <thead>
14 <th>Updated</th>
15 <th>Package</th>
16 <th>Summary</th>
17 </thead>
18 <tbody>
19 {% for dist in dists %}
20 <tr>
21 <td>{{ dist.updated|date:"d/m/y" }}
22 <td><a href="{{ dist.get_pypi_absolute_url }}"/>{{ dist.name }}</a></td>
23 <td>{{ dist.summary|truncatewords:10 }}</td>
24 </tr>
25 {% endfor %}
26 </tbody>
27 </table>
28 {% else %}
29 There were no matches.
30 {% endif %}
31{% endblock content %} \ No newline at end of file
diff --git a/chishop/urls.py b/chishop/urls.py
index ab2e8a9..5a5dd77 100644
--- a/chishop/urls.py
+++ b/chishop/urls.py
@@ -10,7 +10,7 @@ urlpatterns = patterns('')
10# Serve static pages. 10# Serve static pages.
11if settings.LOCAL_DEVELOPMENT: 11if settings.LOCAL_DEVELOPMENT:
12 urlpatterns += patterns("django.views", 12 urlpatterns += patterns("django.views",
13 url(r"%s(?P<path>.*)$" % settings.MEDIA_URL[1:], "static.serve", { 13 url(r"^%s(?P<path>.*)$" % settings.MEDIA_URL[1:], "static.serve", {
14 "document_root": settings.MEDIA_ROOT})) 14 "document_root": settings.MEDIA_ROOT}))
15 15
16urlpatterns += patterns("", 16urlpatterns += patterns("",
diff --git a/djangopypi/forms.py b/djangopypi/forms.py
index 5484747..6a65d37 100644
--- a/djangopypi/forms.py
+++ b/djangopypi/forms.py
@@ -14,5 +14,4 @@ class ProjectForm(forms.ModelForm):
14class ReleaseForm(forms.ModelForm): 14class ReleaseForm(forms.ModelForm):
15 class Meta: 15 class Meta:
16 model = Release 16 model = Release
17 exclude = ['project'] 17 exclude = ['project'] \ No newline at end of file
18
diff --git a/djangopypi/http.py b/djangopypi/http.py
index 01081f7..7be5959 100644
--- a/djangopypi/http.py
+++ b/djangopypi/http.py
@@ -1,4 +1,8 @@
1from django.http import HttpResponse 1from django.http import HttpResponse
2from django.core.files.uploadedfile import SimpleUploadedFile
3from django.utils.datastructures import MultiValueDict
4from django.contrib.auth import authenticate
5
2 6
3class HttpResponseNotImplemented(HttpResponse): 7class HttpResponseNotImplemented(HttpResponse):
4 status_code = 501 8 status_code = 501
@@ -12,4 +16,51 @@ class HttpResponseUnauthorized(HttpResponse):
12 self['WWW-Authenticate'] = 'Basic realm="%s"' % realm 16 self['WWW-Authenticate'] = 'Basic realm="%s"' % realm
13 17
14 18
19def parse_distutils_request(request):
20 raw_post_data = request.raw_post_data
21 sep = raw_post_data.splitlines()[1]
22 items = raw_post_data.split(sep)
23 post_data = {}
24 files = {}
25 for part in filter(lambda e: not e.isspace(), items):
26 item = part.splitlines()
27 if len(item) < 2:
28 continue
29 header = item[1].replace("Content-Disposition: form-data; ", "")
30 kvpairs = header.split(";")
31 headers = {}
32 for kvpair in kvpairs:
33 if not kvpair:
34 continue
35 key, value = kvpair.split("=")
36 headers[key] = value.strip('"')
37 if "name" not in headers:
38 continue
39 content = part[len("\n".join(item[0:2]))+2:len(part)-1]
40 if "filename" in headers:
41 file = SimpleUploadedFile(headers["filename"], content,
42 content_type="application/gzip")
43 files["distribution"] = [file]
44 elif headers["name"] in post_data:
45 post_data[headers["name"]].append(content)
46 else:
47 # Distutils sends UNKNOWN for empty fields (e.g platform)
48 # [russell.sim@gmail.com]
49 if content == 'UNKNOWN':
50 post_data[headers["name"]] = [None]
51 else:
52 post_data[headers["name"]] = [content]
53
54 return MultiValueDict(post_data), MultiValueDict(files)
55
15 56
57def login_basic_auth(request):
58 authentication = request.META.get("HTTP_AUTHORIZATION")
59 if not authentication:
60 return
61 (authmeth, auth) = authentication.split(' ', 1)
62 if authmeth.lower() != "basic":
63 return
64 auth = auth.strip().decode("base64")
65 username, password = auth.split(":", 1)
66 return authenticate(username=username, password=password)
diff --git a/djangopypi/tests.py b/djangopypi/tests.py
index 2d8c305..44ec3ac 100644
--- a/djangopypi/tests.py
+++ b/djangopypi/tests.py
@@ -1,6 +1,10 @@
1import unittest 1import unittest
2import StringIO 2import StringIO
3from djangopypi.views import parse_distutils_request 3from djangopypi.views import parse_distutils_request
4from djangopypi.models import Project, Classifier
5from django.test.client import Client
6from django.core.urlresolvers import reverse
7from django.contrib.auth.models import User
4 8
5def create_post_data(action): 9def create_post_data(action):
6 data = { 10 data = {
@@ -80,11 +84,27 @@ class TestParseWeirdPostData(unittest.TestCase):
80 data = create_post_data("submit") 84 data = create_post_data("submit")
81 raw_post_data = create_request(data) 85 raw_post_data = create_request(data)
82 post, files = parse_distutils_request(MockRequest(raw_post_data)) 86 post, files = parse_distutils_request(MockRequest(raw_post_data))
83 print("post: %s files: %s" % (post, files))
84 self.assertTrue(post) 87 self.assertTrue(post)
85 88
86 for key in post.keys(): 89 for key in post.keys():
87 if isinstance(data[key], list): 90 if isinstance(data[key], list):
88 self.assertEquals(data[key], post.getlist(key)) 91 self.assertEquals(data[key], post.getlist(key))
92 elif data[key] == "UNKNOWN":
93 self.assertTrue(post[key] is None)
89 else: 94 else:
90 self.assertEquals(post[key], data[key]) 95 self.assertEquals(post[key], data[key])
96
97class TestSearch(unittest.TestCase):
98
99 def setUp(self):
100 data = create_post_data("submit")
101 dummy_user = User.objects.create(username='krill', password='12345',
102 email='krill@opera.com')
103 Project.objects.create(name=data['name'], license=data['license'],
104 summary=data["summary"], owner=dummy_user)
105
106
107 def testSearchForPackage(self):
108 client = Client()
109 response = client.post(reverse('djangopypi-search'), {'search_term': 'foo'})
110 self.assertTrue("The quick brown fox jumps over the lazy dog." in response.content)
diff --git a/djangopypi/urls.py b/djangopypi/urls.py
index d6cccb5..79b16be 100644
--- a/djangopypi/urls.py
+++ b/djangopypi/urls.py
@@ -19,5 +19,6 @@ urlpatterns = patterns("djangopypi.views",
19 url(r'^(?P<dist_name>[\w\d_\.\-]+)/$', "show_links", 19 url(r'^(?P<dist_name>[\w\d_\.\-]+)/$', "show_links",
20 {'template_name': 'djangopypi/pypi_show_links.html'}, 20 {'template_name': 'djangopypi/pypi_show_links.html'},
21 name="djangopypi-pypi_show_links"), 21 name="djangopypi-pypi_show_links"),
22) 22
23 23 url(r'^search','search',name='djangopypi-search')
24) \ No newline at end of file
diff --git a/djangopypi/views.py b/djangopypi/views.py
deleted file mode 100644
index 8817f66..0000000
--- a/djangopypi/views.py
+++ /dev/null
@@ -1,229 +0,0 @@
1import cgi
2import os
3
4try:
5 from cStringIO import StringIO
6except ImportError:
7 from StringIO import StringIO
8
9from django.conf import settings
10from django.http import Http404, HttpResponse, HttpResponseBadRequest
11from django.http import QueryDict, HttpResponseForbidden
12from django.shortcuts import render_to_response
13from django.template import RequestContext
14from django.utils.datastructures import MultiValueDict
15from django.utils.translation import ugettext_lazy as _
16from django.core.files.uploadedfile import SimpleUploadedFile
17from django.contrib.auth import authenticate, login
18
19from registration.backends import get_backend
20from registration.forms import RegistrationForm
21
22from djangopypi.models import Project, Classifier, Release, UPLOAD_TO
23from djangopypi.forms import ProjectForm, ReleaseForm
24from djangopypi.http import HttpResponseUnauthorized
25from djangopypi.http import HttpResponseNotImplemented
26from djangopypi.utils import decode_fs
27
28
29ALREADY_EXISTS_FMT = _("""A file named "%s" already exists for %s. To fix """
30 + "problems with that you should create a new release.")
31
32
33def parse_distutils_request(request):
34 raw_post_data = request.raw_post_data
35 sep = raw_post_data.splitlines()[1]
36 items = raw_post_data.split(sep)
37 post_data = {}
38 files = {}
39 for part in filter(lambda e: not e.isspace(), items):
40 item = part.splitlines()
41 if len(item) < 2:
42 continue
43 header = item[1].replace("Content-Disposition: form-data; ", "")
44 kvpairs = header.split(";")
45 headers = {}
46 for kvpair in kvpairs:
47 if not kvpair:
48 continue
49 key, value = kvpair.split("=")
50 headers[key] = value.strip('"')
51 if "name" not in headers:
52 continue
53 content = part[len("\n".join(item[0:2]))+2:len(part)-1]
54 if "filename" in headers:
55 file = SimpleUploadedFile(headers["filename"], content,
56 content_type="application/gzip")
57 files["distribution"] = [file]
58 elif headers["name"] in post_data:
59 post_data[headers["name"]].append(content)
60 else:
61 # Distutils sends UNKNOWN for empty fields (e.g platform)
62 # [russell.sim@gmail.com]
63 if content == 'UNKNOWN':
64 post_data[headers["name"]] = [None]
65 else:
66 post_data[headers["name"]] = [content]
67
68 return MultiValueDict(post_data), MultiValueDict(files)
69
70
71
72def login_basic_auth(request):
73 authentication = request.META.get("HTTP_AUTHORIZATION")
74 if not authentication:
75 return
76 (authmeth, auth) = authentication.split(' ', 1)
77 if authmeth.lower() != "basic":
78 return
79 auth = auth.strip().decode("base64")
80 username, password = auth.split(":", 1)
81 return authenticate(username=username, password=password)
82
83
84def submit_project_or_release(user, post_data, files):
85 """Registers/updates a project or release"""
86 try:
87 project = Project.objects.get(name=post_data['name'])
88 if project.owner != user:
89 return HttpResponseForbidden(
90 "That project is owned by someone else!")
91 except Project.DoesNotExist:
92 project = None
93
94 project_form = ProjectForm(post_data, instance=project)
95 if project_form.is_valid():
96 project = project_form.save(commit=False)
97 project.owner = user
98 project.save()
99 for c in post_data.getlist('classifiers'):
100 classifier, created = Classifier.objects.get_or_create(name=c)
101 project.classifiers.add(classifier)
102 if files:
103 allow_overwrite = getattr(settings,
104 "DJANGOPYPI_ALLOW_VERSION_OVERWRITE", False)
105 try:
106 release = Release.objects.get(version=post_data['version'],
107 project=project,
108 distribution=UPLOAD_TO + '/' +
109 files['distribution']._name)
110 if not allow_overwrite:
111 return HttpResponseForbidden(ALREADY_EXISTS_FMT % (
112 release.filename, release))
113 except Release.DoesNotExist:
114 release = None
115
116 # If the old file already exists, django will append a _ after the
117 # filename, however with .tar.gz files django does the "wrong"
118 # thing and saves it as project-0.1.2.tar_.gz. So remove it before
119 # django sees anything.
120 release_form = ReleaseForm(post_data, files, instance=release)
121 if release_form.is_valid():
122 if release and os.path.exists(release.distribution.path):
123 os.remove(release.distribution.path)
124 release = release_form.save(commit=False)
125 release.project = project
126 release.save()
127 else:
128 return HttpResponseBadRequest(
129 "ERRORS: %s" % release_form.errors)
130 else:
131 return HttpResponseBadRequest("ERRORS: %s" % project_form.errors)
132
133 return HttpResponse()
134
135
136def register_or_upload(request, post_data, files):
137 user = login_basic_auth(request)
138 if not user:
139 return HttpResponseUnauthorized('pypi')
140
141 login(request, user)
142 if not request.user.is_authenticated():
143 return HttpResponseForbidden(
144 "Not logged in, or invalid username/password.")
145
146 return submit_project_or_release(user, post_data, files)
147
148def create_user(request, post_data, files):
149 """Create new user from a distutil client request"""
150 form = RegistrationForm({"username": post_data["name"],
151 "email": post_data["email"],
152 "password1": post_data["password"],
153 "password2": post_data["password"]})
154 if not form.is_valid():
155 # Dist Utils requires error msg in HTTP status: "HTTP/1.1 400 msg"
156 # Which is HTTP/WSGI incompatible, so we're just returning a empty 400.
157 return HttpResponseBadRequest()
158
159 backend = get_backend("registration.backends.default.DefaultBackend")
160 if not backend.registration_allowed(request):
161 return HttpResponseBadRequest()
162 new_user = backend.register(request, **form.cleaned_data)
163 return HttpResponse("OK\n", status=200, mimetype='text/plain')
164
165
166ACTIONS = {
167 # file_upload is the action used with distutils ``sdist`` command.
168 "file_upload": register_or_upload,
169
170 # submit is the :action used with distutils ``register`` command.
171 "submit": register_or_upload,
172
173 # user is the action used when registering a new user
174 "user": create_user,
175}
176
177
178def simple(request, template_name="djangopypi/simple.html"):
179 if request.method == "POST":
180 post_data, files = parse_distutils_request(request)
181 action_name = post_data.get(":action")
182 if action_name not in ACTIONS:
183 return HttpResponseNotImplemented(
184 "The action %s is not implemented" % action_name)
185 return ACTIONS[action_name](request, post_data, files)
186
187 dists = Project.objects.all().order_by("name")
188 context = RequestContext(request, {
189 "dists": dists,
190 "title": 'Package Index',
191 })
192
193 return render_to_response(template_name, context_instance=context)
194
195
196def show_links(request, dist_name,
197 template_name="djangopypi/show_links.html"):
198 try:
199 project = Project.objects.get(name=dist_name)
200 releases = project.releases.all().order_by('-version')
201 except Project.DoesNotExist:
202 raise Http404
203
204 context = RequestContext(request, {
205 "dist_name": dist_name,
206 "releases": releases,
207 "project": project,
208 "title": project.name,
209 })
210
211 return render_to_response(template_name, context_instance=context)
212
213
214def show_version(request, dist_name, version,
215 template_name="djangopypi/show_version.html"):
216 try:
217 release = Project.objects.get(name=dist_name).releases \
218 .get(version=version)
219 except Project.DoesNotExist:
220 raise Http404()
221
222 context = RequestContext(request, {
223 "dist_name": dist_name,
224 "version": version,
225 "release": release,
226 "title": dist_name,
227 })
228
229 return render_to_response(template_name, context_instance=context)
diff --git a/djangopypi/views/__init__.py b/djangopypi/views/__init__.py
new file mode 100644
index 0000000..1438c54
--- /dev/null
+++ b/djangopypi/views/__init__.py
@@ -0,0 +1,76 @@
1from django.http import Http404
2from django.shortcuts import render_to_response
3from django.template import RequestContext
4
5from djangopypi.models import Project, Release
6from djangopypi.http import HttpResponseNotImplemented
7from djangopypi.http import parse_distutils_request
8from djangopypi.views.dists import register_or_upload
9from djangopypi.views.users import create_user
10from djangopypi.views.search import search
11
12
13ACTIONS = {
14 # file_upload is the action used with distutils ``sdist`` command.
15 "file_upload": register_or_upload,
16
17 # submit is the :action used with distutils ``register`` command.
18 "submit": register_or_upload,
19
20 # user is the action used when registering a new user
21 "user": create_user,
22}
23
24
25def simple(request, template_name="djangopypi/simple.html"):
26 if request.method == "POST":
27 post_data, files = parse_distutils_request(request)
28 action_name = post_data.get(":action")
29 if action_name not in ACTIONS:
30 return HttpResponseNotImplemented(
31 "The action %s is not implemented" % action_name)
32 return ACTIONS[action_name](request, post_data, files)
33
34 dists = Project.objects.all().order_by("name")
35 context = RequestContext(request, {
36 "dists": dists,
37 "title": 'Package Index',
38 })
39
40 return render_to_response(template_name, context_instance=context)
41
42
43def show_links(request, dist_name,
44 template_name="djangopypi/show_links.html"):
45 try:
46 project = Project.objects.get(name=dist_name)
47 releases = project.releases.all().order_by('-version')
48 except Project.DoesNotExist:
49 raise Http404
50
51 context = RequestContext(request, {
52 "dist_name": dist_name,
53 "releases": releases,
54 "project": project,
55 "title": project.name,
56 })
57
58 return render_to_response(template_name, context_instance=context)
59
60
61def show_version(request, dist_name, version,
62 template_name="djangopypi/show_version.html"):
63 try:
64 project = Project.objects.get(name=dist_name)
65 release = project.releases.get(version=version)
66 except (Project.DoesNotExist, Release.DoesNotExist):
67 raise Http404()
68
69 context = RequestContext(request, {
70 "dist_name": dist_name,
71 "version": version,
72 "release": release,
73 "title": dist_name,
74 })
75
76 return render_to_response(template_name, context_instance=context)
diff --git a/djangopypi/views/dists.py b/djangopypi/views/dists.py
new file mode 100644
index 0000000..9e4a146
--- /dev/null
+++ b/djangopypi/views/dists.py
@@ -0,0 +1,79 @@
1import os
2
3from django.conf import settings
4from django.http import (HttpResponse, HttpResponseForbidden,
5 HttpResponseBadRequest)
6from django.utils.translation import ugettext_lazy as _
7from django.contrib.auth import login
8
9from djangopypi.http import login_basic_auth, HttpResponseUnauthorized
10from djangopypi.forms import ProjectForm, ReleaseForm
11from djangopypi.models import Project, Release, Classifier, UPLOAD_TO
12
13ALREADY_EXISTS_FMT = _(
14 "A file named '%s' already exists for %s. Please create a new release.")
15
16
17def submit_project_or_release(user, post_data, files):
18 """Registers/updates a project or release"""
19 try:
20 project = Project.objects.get(name=post_data['name'])
21 if project.owner != user:
22 return HttpResponseForbidden(
23 "That project is owned by someone else!")
24 except Project.DoesNotExist:
25 project = None
26
27 project_form = ProjectForm(post_data, instance=project)
28 if project_form.is_valid():
29 project = project_form.save(commit=False)
30 project.owner = user
31 project.save()
32 for c in post_data.getlist('classifiers'):
33 classifier, created = Classifier.objects.get_or_create(name=c)
34 project.classifiers.add(classifier)
35 if files:
36 allow_overwrite = getattr(settings,
37 "DJANGOPYPI_ALLOW_VERSION_OVERWRITE", False)
38 try:
39 release = Release.objects.get(version=post_data['version'],
40 project=project,
41 distribution=UPLOAD_TO + '/' +
42 files['distribution']._name)
43 if not allow_overwrite:
44 return HttpResponseForbidden(ALREADY_EXISTS_FMT % (
45 release.filename, release))
46 except Release.DoesNotExist:
47 release = None
48
49 # If the old file already exists, django will append a _ after the
50 # filename, however with .tar.gz files django does the "wrong"
51 # thing and saves it as project-0.1.2.tar_.gz. So remove it before
52 # django sees anything.
53 release_form = ReleaseForm(post_data, files, instance=release)
54 if release_form.is_valid():
55 if release and os.path.exists(release.distribution.path):
56 os.remove(release.distribution.path)
57 release = release_form.save(commit=False)
58 release.project = project
59 release.save()
60 else:
61 return HttpResponseBadRequest(
62 "ERRORS: %s" % release_form.errors)
63 else:
64 return HttpResponseBadRequest("ERRORS: %s" % project_form.errors)
65
66 return HttpResponse()
67
68
69def register_or_upload(request, post_data, files):
70 user = login_basic_auth(request)
71 if not user:
72 return HttpResponseUnauthorized('pypi')
73
74 login(request, user)
75 if not request.user.is_authenticated():
76 return HttpResponseForbidden(
77 "Not logged in, or invalid username/password.")
78
79 return submit_project_or_release(user, post_data, files)
diff --git a/djangopypi/views/search.py b/djangopypi/views/search.py
new file mode 100644
index 0000000..5d6a76b
--- /dev/null
+++ b/djangopypi/views/search.py
@@ -0,0 +1,24 @@
1from django.template import RequestContext
2from django.shortcuts import render_to_response
3from django.db.models.query import Q
4
5from djangopypi.models import Project
6
7
8def _search_query(q):
9 return Q(name__contains=q) | Q(summary__contains=q)
10
11
12def search(request, template="djangopypi/search_results.html"):
13 context = RequestContext(request, {"dists": None, "search_term": ""})
14
15 if request.method == "POST":
16 search_term = context["search_term"] = request.POST.get("search_term")
17 if search_term:
18 query = _search_query(search_term)
19 context["dists"] = Project.objects.filter(query)
20
21 if context["dists"] is None:
22 context["dists"] = Project.objects.all()
23
24 return render_to_response(template, context_instance=context)
diff --git a/djangopypi/views/users.py b/djangopypi/views/users.py
new file mode 100644
index 0000000..a58ac3e
--- /dev/null
+++ b/djangopypi/views/users.py
@@ -0,0 +1,24 @@
1from django.http import HttpResponse, HttpResponseBadRequest
2
3from registration.forms import RegistrationForm
4from registration.backends import get_backend
5
6DEFAULT_BACKEND = "registration.backends.default.DefaultBackend"
7
8
9def create_user(request, post_data, files, backend_name=DEFAULT_BACKEND):
10 """Create new user from a distutil client request"""
11 form = RegistrationForm({"username": post_data["name"],
12 "email": post_data["email"],
13 "password1": post_data["password"],
14 "password2": post_data["password"]})
15 if not form.is_valid():
16 # Dist Utils requires error msg in HTTP status: "HTTP/1.1 400 msg"
17 # Which is HTTP/WSGI incompatible, so we're just returning a empty 400.
18 return HttpResponseBadRequest()
19
20 backend = get_backend(backend_name)
21 if not backend.registration_allowed(request):
22 return HttpResponseBadRequest()
23 new_user = backend.register(request, **form.cleaned_data)
24 return HttpResponse("OK\n", status=200, mimetype='text/plain')