Merge branch 'master' into test_submission_views_365

This commit is contained in:
Chris Moylan 2011-06-22 21:40:17 -05:00
commit dcebe4b032
24 changed files with 259 additions and 81 deletions

View File

@ -80,10 +80,10 @@ doing and why.
How to send us your changes How to send us your changes
--------------------------- ---------------------------
There are three ways to let us know how to get it: There are two ways to let us know how to get it:
1. (preferred) **push changes to publicly available git clone and let 1. *(preferred)* **push changes to publicly available git clone and
us know where to find it** let us know where to find it**
Push your feature/bugfix/issue branch to your publicly available Push your feature/bugfix/issue branch to your publicly available
git clone and add a comment to the issue with the url for your git clone and add a comment to the issue with the url for your
@ -93,14 +93,22 @@ There are three ways to let us know how to get it:
Run:: Run::
git format-patch -o patches <remote>/master git format-patch --stdout <remote>/master > issue_<number>.patch
Then tar up the newly created ``patches`` directory and attach the ``format-patch`` creates a patch of all the commits that are in
directory to the issue. your branch that aren't in ``<remote>/master``. The ``--stdout``
flag causes all this output to go to stdout where it's redirected
to a file named ``issue_<number>.patch``. That file should be
based on the issue you're working with. For example,
``issue_42.patch`` is a good filename and ``issue_42_rev2.patch``
is good if you did a revision of it.
Having said all that, the filename isn't wildly important.
Example workflow Example workflow
================ ================
Here's an example workflow. Here's an example workflow.
@ -124,20 +132,30 @@ Slartibartfast does the following:
git fetch --all -p git fetch --all -p
This tells ``git fetch`` to fetch all the recent data from all of
the remotes (``--all``) and prune any branches that have been
deleted in the remotes (``-p``).
2. Creates a branch from the tip of the MediaGoblin repository (the 2. Creates a branch from the tip of the MediaGoblin repository (the
remote is named ``gmg``) master branch called ``bug42_meaning_of_life``:: remote is named ``gmg``) master branch called ``bug42_meaning_of_life``::
git checkout -b bug42_meaning_of_life gmg/master git checkout -b bug42_meaning_of_life gmg/master
This creates a new branch (``-b``) named ``bug42_meaning_of_life`` based
on the tip of the ``master`` branch of the remote named ``gmg`` and checks
it out.
3. Slartibartfast works hard on his changes in the ``bug42_meaning_of_life`` 3. Slartibartfast works hard on his changes in the ``bug42_meaning_of_life``
branch. When done, he wants to notify us that he has made changes branch. When done, he wants to notify us that he has made changes
he wants us to see. he wants us to see.
4. Slartibartfast pushes his changes to his clone (the remote is named 4. Slartibartfast pushes his changes to his clone::
``origin``)::
git push origin bug42_meaning_of_life --set-upstream git push origin bug42_meaning_of_life --set-upstream
This pushes the changes in the ``bug42_meaning_of_life`` branch to the
remote named ``origin``.
5. Slartibartfast adds a comment to issue 42 with the url for his 5. Slartibartfast adds a comment to issue 42 with the url for his
repository and the name of the branch he put the code in. He also repository and the name of the branch he put the code in. He also
explains what he did and why it addresses the issue. explains what he did and why it addresses the issue.

View File

@ -2,6 +2,10 @@
GNU MediaGoblin GNU MediaGoblin
================= =================
.. contents:: Sections
:local:
What is GNU MediaGoblin What is GNU MediaGoblin
======================= =======================

View File

@ -50,5 +50,20 @@ class MediaEntryMigration(DocumentMigration):
'description_html': cleaned_markdown_conversion( 'description_html': cleaned_markdown_conversion(
doc['description'])}} doc['description'])}}
class UserMigration(DocumentMigration):
MIGRATE_CLASSES = ['MediaEntry'] def allmigration01_add_bio_and_url_profile(self):
"""
User can elaborate profile with home page and biography
"""
self.target = {'url': {'$exists': False},
'bio': {'$exists': False}}
if not self.status:
for doc in self.collection.find(self.target):
self.update = {
'$set': {'url': '',
'bio': ''}}
self.collection.update(
self.target, self.update, multi=True, safe=True)
MIGRATE_CLASSES = ['MediaEntry', 'User']

View File

@ -46,6 +46,8 @@ class User(Document):
'status': unicode, 'status': unicode,
'verification_key': unicode, 'verification_key': unicode,
'is_admin': bool, 'is_admin': bool,
'url' : unicode,
'bio' : unicode
} }
required_fields = ['username', 'created', 'pw_hash', 'email'] required_fields = ['username', 'created', 'pw_hash', 'email']
@ -56,6 +58,8 @@ class User(Document):
'status': u'needs_email_verification', 'status': u'needs_email_verification',
'verification_key': lambda: unicode(uuid.uuid4()), 'verification_key': lambda: unicode(uuid.uuid4()),
'is_admin': False} 'is_admin': False}
migration_handler = migrations.UserMigration
def check_login(self, password): def check_login(self, password):
""" """

View File

@ -17,4 +17,5 @@
# Imports that other modules might use # Imports that other modules might use
from pymongo import DESCENDING from pymongo import DESCENDING
from pymongo.errors import InvalidId
from mongokit import ObjectId from mongokit import ObjectId

View File

@ -15,11 +15,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from bson.errors import InvalidId
from webob import exc from webob import exc
from mediagoblin.util import redirect from mediagoblin.util import redirect
from mediagoblin.db.util import ObjectId from mediagoblin.db.util import ObjectId, InvalidId
def _make_safe(decorator, original): def _make_safe(decorator, original):

View File

@ -25,3 +25,10 @@ class EditForm(wtforms.Form):
slug = wtforms.TextField( slug = wtforms.TextField(
'Slug') 'Slug')
description = wtforms.TextAreaField('Description of this work') description = wtforms.TextAreaField('Description of this work')
class EditProfileForm(wtforms.Form):
bio = wtforms.TextAreaField('Bio',
[wtforms.validators.Length(min=0, max=500)])
url = wtforms.TextField(
'Website',
[wtforms.validators.URL(message='Improperly formed URL')])

View File

@ -19,4 +19,5 @@ from routes.route import Route
edit_routes = [ edit_routes = [
# Media editing view handled in user_pages/routing.py # Media editing view handled in user_pages/routing.py
] Route('mediagoblin.edit.profile', '/profile/',
controller="mediagoblin.edit.views:edit_profile")]

View File

@ -68,3 +68,25 @@ def edit_media(request, media):
'mediagoblin/edit/edit.html', 'mediagoblin/edit/edit.html',
{'media': media, {'media': media,
'form': form}) 'form': form})
@require_active_login
def edit_profile(request):
user = request.user
form = forms.EditProfileForm(request.POST,
url = user.get('url'),
bio = user.get('bio'))
if request.method == 'POST' and form.validate():
user['url'] = request.POST['url']
user['bio'] = request.POST['bio']
user.save()
return redirect(request, "index", user=user['username'])
return render_to_response(
request,
'mediagoblin/edit/edit_profile.html',
{'user': user,
'form': form})

View File

@ -69,23 +69,6 @@ a.mediagoblin_logo:hover {
float:right; float:right;
} }
.button {
font-family:'Carter One', arial, serif;
height:32px;
min-width:99px;
background-color:#86d4b1;
box-shadow:0px 0px 4px #000;
border-radius:5px;
border:none;
color:#272727;
margin:10px;
font-size:1em;
display:block;
text-align:center;
padding-left:11px;
padding-right:11px;
}
/* common website elements */ /* common website elements */
.dotted_line { .dotted_line {
@ -97,6 +80,28 @@ a.mediagoblin_logo:hover {
margin-top:-20px; margin-top:-20px;
} }
.button {
font-family:'Carter One', arial, serif;
height:32px;
min-width:99px;
background-color:#86d4b1;
background-image: -webkit-gradient(linear, left top, left bottom, from(#86d4b1), to(#62caa2));
background-image: -webkit-linear-gradient(top, #86d4b1, #62caa2);
background-image: -moz-linear-gradient(top, #86d4b1, #62caa2);
background-image: -ms-linear-gradient(top, #86d4b1, #62caa2);
background-image: -o-linear-gradient(top, #86d4b1, #62caa2);
background-image: linear-gradient(top, #86d4b1, #62caa2);
box-shadow:0px 0px 4px #000;
border-radius:5px;
border:none;
color:#272727;
margin:10px 0px 10px 15px;
font-size:1em;
text-align:center;
padding-left:11px;
padding-right:11px;
}
/* forms */ /* forms */
.form_box { .form_box {
@ -104,8 +109,9 @@ a.mediagoblin_logo:hover {
margin-left:auto; margin-left:auto;
margin-right:auto; margin-right:auto;
background-color:#393939; background-color:#393939;
padding:0px 83px 30px 83px; background-image:url("../images/background_lines.png");
border-top:5px solid #d49086; background-repeat:repeat-x;
padding:1px 83px 30px 83px;
font-size:18px; font-size:18px;
} }
@ -113,6 +119,11 @@ a.mediagoblin_logo:hover {
width:600px; width:600px;
} }
.edit_box {
width:600px;
background-image:url("../images/background_edit.png");
}
.form_box h1 { .form_box h1 {
font-size:28px; font-size:28px;
} }
@ -139,6 +150,10 @@ a.mediagoblin_logo:hover {
margin-bottom:8px; margin-bottom:8px;
} }
.form_submit_buttons {
text-align:right;
}
/* media pages */ /* media pages */
img.media_image { img.media_image {
@ -147,14 +162,17 @@ img.media_image {
margin-right:auto; margin-right:auto;
} }
li.media_thumbnail { ul.media_thumbnail {
width: 200px; padding:0px;
min-height: 250px; }
display: -moz-inline-stack;
display: inline-block; li.media_thumbnail {
vertical-align: top; width:200px;
margin: 5px; height:133px;
zoom: 1; display:-moz-inline-stack;
*display: inline; display:inline-block;
_height: 250px; vertical-align:top;
margin:0px 10px 10px 0px;
zoom:1;
. *display:inline;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

View File

@ -35,6 +35,8 @@
<div class="mediagoblin_header_right"> <div class="mediagoblin_header_right">
{% if request.user %} {% if request.user %}
{{ request.user['username'] }}'s account {{ request.user['username'] }}'s account
<a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
user= request.user['username']) }}">home</a>
<a href="{{ request.urlgen('mediagoblin.user_pages.user_gallery', <a href="{{ request.urlgen('mediagoblin.user_pages.user_gallery',
user= request.user['username']) }}">gallery</a> user= request.user['username']) }}">gallery</a>
(<a href="{{ request.urlgen('mediagoblin.auth.logout') }}">logout</a>) (<a href="{{ request.urlgen('mediagoblin.auth.logout') }}">logout</a>)

View File

@ -20,19 +20,21 @@
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %} {% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_content %} {% block mediagoblin_content %}
<h1>Edit details for {{ media.title }}</h1>
<form action="{{ request.urlgen('mediagoblin.edit.edit_media', <form action="{{ request.urlgen('mediagoblin.edit.edit_media',
user= media.uploader().username, user= media.uploader().username,
media= media._id) }}" media= media._id) }}"
method="POST" enctype="multipart/form-data"> method="POST" enctype="multipart/form-data">
<div class="submit_box form_box"> <div class="edit_box form_box">
<h1>Editing {{ media.title }}</h1>
{{ wtforms_util.render_divs(form) }} {{ wtforms_util.render_divs(form) }}
<div class="form_submit_buttons"> <div class="form_submit_buttons">
<input type="submit" value="submit" class="button" /> <a href="{{ media.url_for_self(request.urlgen) }}">Cancel</a>
<input type="submit" value="Save changes" class="button" />
</div> </div>
<img src="{{ request.app.public_store.file_url(
media['media_files']['thumb']) }}" />
</div> </div>
</form> </form>
<img src="{{ request.app.public_store.file_url(
media['media_files']['thumb']) }}" />
{% endblock %} {% endblock %}

View File

@ -0,0 +1,35 @@
{#
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011 Free Software Foundation, Inc
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% extends "mediagoblin/base.html" %}
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_content %}
<form action="{{ request.urlgen('mediagoblin.edit.profile',
user=user.username) }}"
method="POST" enctype="multipart/form-data">
<div class="edit_box form_box">
<h1>Editing {{ user['username'] }}'s profile</h1>
{{ wtforms_util.render_divs(form) }}
<div class="form_submit_buttons">
<input type="submit" value="submit" class="button" />
</div>
</div>
</form>
{% endblock %}

View File

@ -22,20 +22,19 @@
<h1>{% trans %}Welcome to GNU MediaGoblin!{% endtrans %}</h1> <h1>{% trans %}Welcome to GNU MediaGoblin!{% endtrans %}</h1>
{% if request.user %} {% if request.user %}
<p> <p>
<a href="{{ request.urlgen('mediagoblin.submit.start') }}">Submit an item</a>. <a href="{{ request.urlgen('mediagoblin.submit.start') }}">Submit an item</a>
</p> <a href="{{ request.urlgen('mediagoblin.edit.profile') }}">Edit profile</a>
</p>
{% else %} {% else %}
<p> <p>
If you have an account, you can If you have an account, you can
<a href="{{ request.urlgen('mediagoblin.auth.login') }}">Login</a>. <a href="{{ request.urlgen('mediagoblin.auth.login') }}">Login</a>.
</p> </p>
<p> <p>
If you don't have an account, please If you don't have an account, please
<a href="{{ request.urlgen('mediagoblin.auth.register') }}">Register</a>. <a href="{{ request.urlgen('mediagoblin.auth.register') }}">Register</a>.
</p> </p>
{% endif %} {% endif %}
{# temporarily, an "image gallery" that isn't one really ;) #} {# temporarily, an "image gallery" that isn't one really ;) #}

View File

@ -27,7 +27,7 @@
<h1>Submit yer media</h1> <h1>Submit yer media</h1>
{{ wtforms_util.render_divs(submit_form) }} {{ wtforms_util.render_divs(submit_form) }}
<div class="form_submit_buttons"> <div class="form_submit_buttons">
<input type="submit" value="submit" class="button" /> <input type="submit" value="Submit" class="button" />
</div> </div>
</div> </div>
</form> </form>

View File

@ -34,10 +34,12 @@
by by
<a href="{{ request.urlgen('mediagoblin.user_pages.user_home', <a href="{{ request.urlgen('mediagoblin.user_pages.user_home',
user= media.uploader().username) }}"> user= media.uploader().username) }}">
{{- media.uploader().username }}</a></p> {{- media.uploader().username }}</a></p>
<p><a href="{{ request.urlgen('mediagoblin.edit.edit_media', {% if media['uploader'] == request.user['_id'] %}
user= media.uploader().username, <p><a href="{{ request.urlgen('mediagoblin.edit.edit_media',
media= media._id) }}">Edit</a></p> user= media.uploader().username,
media= media._id) }}">Edit</a></p>
{% endif %}
{% else %} {% else %}
<p>Sorry, no such media found.<p/> <p>Sorry, no such media found.<p/>
{% endif %} {% endif %}

View File

@ -27,6 +27,8 @@
{% block mediagoblin_content -%} {% block mediagoblin_content -%}
{% if user %} {% if user %}
<h1>User page for '{{ user.username }}'</h1> <h1>User page for '{{ user.username }}'</h1>
{% include "mediagoblin/utils/profile.html" %}
{% include "mediagoblin/utils/object_gallery.html" %} {% include "mediagoblin/utils/object_gallery.html" %}

View File

@ -19,7 +19,7 @@
{% block object_gallery_content -%} {% block object_gallery_content -%}
<div> <div>
{% if media_entries %} {% if media_entries %}
<ul> <ul class="media_thumbnail">
{% for entry in media_entries %} {% for entry in media_entries %}
<li class="media_thumbnail"> <li class="media_thumbnail">
<a href="{{ entry.url_for_self(request.urlgen) }}"> <a href="{{ entry.url_for_self(request.urlgen) }}">

View File

@ -0,0 +1,35 @@
{#
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2011 Free Software Foundation, Inc
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{% block profile_content -%}
<div>
<ul>
{% if user.url %}
<li>
<a href="{{ user.url }}">homepage</a>
</li>
{% endif %}
{% if user.bio %}
<li>
{{ user.bio }}
</li>
{% endif %}
</ul>
</div>
{% endblock %}

View File

@ -16,12 +16,17 @@
from mediagoblin import mg_globals from mediagoblin import mg_globals
from mediagoblin.tests.tools import (
MEDIAGOBLIN_TEST_DB_NAME, suicide_if_bad_celery_environ)
def setup_package(): def setup_package():
pass suicide_if_bad_celery_environ()
def teardown_package(): def teardown_package():
if mg_globals.db_connection: if ((mg_globals.db_connection
print "Killing db ..." and mg_globals.database.name == MEDIAGOBLIN_TEST_DB_NAME)):
mg_globals.db_connection.drop_database(mg_globals.database.name) print "Killing db ..."
print "... done" mg_globals.db_connection.drop_database(MEDIAGOBLIN_TEST_DB_NAME)
print "... done"

View File

@ -28,7 +28,7 @@ from mediagoblin.decorators import _make_safe
from mediagoblin.db.open import setup_connection_and_db_from_config from mediagoblin.db.open import setup_connection_and_db_from_config
MEDIAGOBLIN_TEST_DB_NAME = '__mediagoblinunittests__' MEDIAGOBLIN_TEST_DB_NAME = u'__mediagoblin_tests__'
TEST_SERVER_CONFIG = pkg_resources.resource_filename( TEST_SERVER_CONFIG = pkg_resources.resource_filename(
'mediagoblin.tests', 'test_paste.ini') 'mediagoblin.tests', 'test_paste.ini')
TEST_APP_CONFIG = pkg_resources.resource_filename( TEST_APP_CONFIG = pkg_resources.resource_filename(
@ -42,17 +42,23 @@ USER_DEV_DIRECTORIES_TO_SETUP = [
'media/public', 'media/queue', 'media/public', 'media/queue',
'beaker/sessions/data', 'beaker/sessions/lock'] 'beaker/sessions/data', 'beaker/sessions/lock']
BAD_CELERY_MESSAGE = """\
Sorry, you *absolutely* must run nosetests with the
mediagoblin.celery_setup.from_tests module. Like so:
$ CELERY_CONFIG_MODULE=mediagoblin.celery_setup.from_tests ./bin/nosetests"""
class BadCeleryEnviron(Exception): pass class BadCeleryEnviron(Exception): pass
def get_test_app(dump_old_app=True): def suicide_if_bad_celery_environ():
if not os.environ.get('CELERY_CONFIG_MODULE') == \ if not os.environ.get('CELERY_CONFIG_MODULE') == \
'mediagoblin.celery_setup.from_tests': 'mediagoblin.celery_setup.from_tests':
raise BadCeleryEnviron( raise BadCeleryEnviron(BAD_CELERY_MESSAGE)
u"Sorry, you *absolutely* must run nosetests with the\n"
u"mediagoblin.celery_setup.from_tests module. Like so:\n"
u"$ CELERY_CONFIG_MODULE=mediagoblin.celery_setup.from_tests ./bin/nosetests") def get_test_app(dump_old_app=True):
suicide_if_bad_celery_environ()
global MGOBLIN_APP global MGOBLIN_APP
global CELERY_SETUP global CELERY_SETUP
@ -78,6 +84,7 @@ def get_test_app(dump_old_app=True):
# @@: For now we're dropping collections, but we could also just # @@: For now we're dropping collections, but we could also just
# collection.remove() ? # collection.remove() ?
connection, db = setup_connection_and_db_from_config(app_config) connection, db = setup_connection_and_db_from_config(app_config)
assert db.name == MEDIAGOBLIN_TEST_DB_NAME
collections_to_wipe = [ collections_to_wipe = [
collection collection
@ -87,10 +94,6 @@ def get_test_app(dump_old_app=True):
for collection in collections_to_wipe: for collection in collections_to_wipe:
db.drop_collection(collection) db.drop_collection(collection)
# Don't need these anymore...
del(connection)
del(db)
# TODO: Drop and recreate indexes # TODO: Drop and recreate indexes
# setup app and return # setup app and return

View File

@ -373,6 +373,10 @@ HTML_CLEANER = Cleaner(
def clean_html(html): def clean_html(html):
# clean_html barfs on an empty string
if not html:
return u''
return HTML_CLEANER.clean_html(html) return HTML_CLEANER.clean_html(html)