[editpage] Basic information (Bug 604151)

This commit is contained in:
Gregory Koberger 2010-10-14 21:12:56 -07:00
Родитель 50f3714460
Коммит 7aa9ca34c3
11 изменённых файлов: 276 добавлений и 297 удалений

Просмотреть файл

@ -3,7 +3,24 @@ from django import forms
import happyforms
from addons.models import Addon
from translations.widgets import TranslationTextInput
from amo.utils import slug_validator
from tower import ugettext as _
from translations.widgets import TranslationTextInput, TranslationTextarea
class AddonFormBasic(happyforms.ModelForm):
name = forms.CharField(widget=TranslationTextInput, max_length=70)
slug = forms.CharField(max_length=30)
summary = forms.CharField(widget=TranslationTextarea, max_length=250)
def clean_slug(self):
target = self.cleaned_data['slug']
slug_validator(target, lower=False)
if self.cleaned_data['slug'] != self.instance.slug:
if Addon.objects.filter(slug=target).exists():
raise forms.ValidationError(_('This slug is already in use.'))
return target
class AddonForm(happyforms.ModelForm):

Просмотреть файл

@ -7,6 +7,7 @@ import time
from django.conf import settings
from django.db import models
from django.db.models import Q, Sum, Max
from django.utils.translation import trans_real as translation
import caching.base as caching
import commonware.log
@ -92,6 +93,8 @@ class AddonManager(amo.models.ManagerBase):
class Addon(amo.models.ModelBase):
STATUS_CHOICES = amo.STATUS_CHOICES.items()
LOCALES = [(translation.to_language(k), v) for k, v in
settings.LANGUAGES.items()]
guid = models.CharField(max_length=255, unique=True, null=True)
slug = models.CharField(max_length=30)

Просмотреть файл

@ -28,283 +28,11 @@
<section class="primary" role="main">
<div id="edit-addon">
<h3>
{{ _('Basic Information') }}
<input type="submit" value="{{ _('Edit') }}">
</h3>
<div class="item">
<table>
<caption>{{ _('Basic Information for {0}')|f(addon.name) }}</caption>
<tbody>
<tr>
<th>{{ _('Name') }}</th>
<td> {{ addon.name }}</td>
</tr>
<tr>
<th>
{{ tip(_("Add-on URL"),
_("Choose a short, unique URL slug for your add-on.")) }}
</th>
<td>
{# TODO: Eventually this needs a slug. #}
<a href="{{ addon.get_url_path() }}">
{{ settings.SITE_URL }}/&hellip;/
{{ addon.id }}</a>
</td>
</tr>
<tr>
<th>
{{ tip(_("Summary"),
_("A short explanation of your add-on's basic functionality
that is displayed in search and browse listings, as well as
at the top of your add-on's details page.")) }}
</th>
<td>
{{ addon.summary|nl2br }}
</td>
</tr>
<tr>
<th>
{{ tip(_("Categories"),
_("Categories are the primary way users browse through add-ons.
Choose any that fit your add-on's functionality for the most
exposure.")) }}
</th>
<td>
{{ addon.categories.all()|join(' &middot; ')|safe }}
</td>
</tr>
<tr>
<th>
{{ tip(_("Tags"),
_("Tags help users find your add-on and should be short
descriptors such as tabs, toolbar, or twitter.")) }}
</th>
<td>
{% call empty_unless(tags_dev) %}
{{ tags_dev|join(', ') }}
{% endcall %}
</td>
</tr>
<tr>
<th>{{ _('User Tags') }}</th>
<td>
{% call empty_unless(tags_user) %}
{{ tags_user|join(', ') }}
{% endcall %}
</td>
</tr>
</tbody>
</table>
</div>
<h3>
{{ _('Images and Videos') }} <input type="submit" value="{{ _('Edit') }}">
</h3>
<div class="item">
<table>
<caption>{{ _('Add-on Details for {0}')|f(addon.name) }}</caption>
<tbody>
<tr>
<th>
{{ tip(_("Add-on Icon"),
_("Upload an icon for your add-on or choose from one of ours.
The icon is displayed nearly everywhere your add-on is.
Uploaded images will be resized to 32 x 32 pixels and must be
one of the following image types: .png, .jpg, .gif")) }}
</th>
<td>
<img src="{{ addon.icon_url }}">
</td>
</tr>
<tr>
<th>{{ _('Screenshots and Videos') }}</th>
<td>
{% if previews %}
{% for preview in preview %}
<img src="{{ preview.thumbnail_url }}">
{% endfor %}
{% endif %}
</td>
</tr>
</tbody>
</table>
<div class="edit-addon-section" id="edit-addon-basic">
{% include 'devhub/includes/addon_edit_basic.html' %}
</div>
<h3>
{{ _('Add-on Details') }} <input type="submit" value="{{ _('Edit') }}">
</h3>
<div class="item">
<table>
<caption>{{ _('Add-on Details for {0}')|f(addon.name) }}</caption>
<tbody>
<tr>
<th>
{{ tip(_("Description"),
_("A longer explanation of features, functionality, and other
relevant information. This field is only displayed on the
add-on's details page.")) }}
</th>
<td>
{% call empty_unless(addon.description) %}
{{ addon.description }}
{% endcall %}
</td>
</tr>
<tr>
<th>
{{ tip(_("Default Locale"),
_("Information about your add-on is displayed in this locale
unless you override it with a locale-specific translation.")) }}
</th>
<td>
{{ addon.default_locale }}
</td>
</tr>
<tr>
<th>
{{ tip(_("Homepage"),
_("If your add-on has another homepage, enter its address here.
If your website is localized into other languages, multiple
translations of this field can be added.")) }}
</th>
<td>
{% call empty_unless(addon.homepage) %}
{{ addon.homepage }}
{% endcall %}
</td>
</tr>
</tbody>
</table>
</div>
<h3>
{{ _('Support Information') }} <input type="submit" value="{{ _('Edit') }}">
</h3>
<div class="item">
<table>
<caption>{{ _('Support Information for {0}')|f(addon.name) }}</caption>
<tbody>
<tr>
<th>
{{ tip(_("Email"),
_("If you wish to display an e-mail address for support inquiries,
enter it here. If you have different addresses for each language,
multiple translations of this field can be added.")) }}
</th>
<td>
{% call empty_unless(addon.support_email) %}
{{ addon.support_email }}
{% endcall %}
</td>
</tr>
<tr>
<th>
{{ tip(_("Website"),
_("If your add-on has a support website or forum, enter its
address here. If your website is localized into other
languages, multiple translations of this field can be added.")) }}
</th>
<td>
{% call empty_unless(addon.support_url) %}
{{ addon.support_url }}
{% endcall %}
</td>
</tr>
<tr>
<th>{{ _('Get Satisfaction') }}</th>
<td>
{% if addon.get_satisfaction_company %}
{{ addon.get_satisfaction_company }}
{% if addon.get_satisfaction_product %}
&raquo; {{ addon.get_satisfaction_product }}
{% endif %}
{% else %}
<span class="empty">{{ _('Not Used') }}</span>
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
<h3>
{{ _('Technical Details') }} <input type="submit" value="{{ _('Edit') }}">
</h3>
<div class="item">
<table>
<caption>{{ _('Technical Details for {0}')|f(addon.name) }}</caption>
<tbody>
<tr>
<th>
{{ tip(_("Developer Comments"),
_("Any information end users may want to know that isn't
necessarily applicable to the add-on summary or description.
Common uses include listing known major bugs, information on
how to report bugs, anticipated release date of a new version,
etc.")) }}
</th>
<td>
{% call empty_unless(addon.developer_comments) %}
{{ addon.developer_comments|nl2br }}
{% endcall %}
</td>
</tr>
<tr>
<th>{{ _('Required Add-ons') }}</th>
<td>
{# TODO(gkoberger) #}
<strong>{{ _('Coming Soon') }}</strong>
</td>
</tr>
<tr>
<th>
{{ tip(_("Add-on flags"),
_("These flags are used to classify add-ons.")) }}
</th>
<td>
{# TODO(gkoberger) #}
<strong>{{ _('Coming Soon') }}</strong>
</td>
</tr>
<tr>
<th>
{{ tip(_("View source?"),
_("Whether the source of your add-on can be displayed in our
online viewer.")) }}
</th>
<td>
{% if addon.view_source %}
{{ _("Yes, this add-on's source code is publicly viewable") }}
{% else %}
{{ _("No, this add-on's source code is not publicly viewable") }}
{% endif %}
</td>
</tr>
<tr>
<th>
<abbr title="{{ _('Universally unique identifier') }}" class="label">
{{ _('UUID') }}
</abbr>
<span class="help tooltip" title="{% trans -%}The UUID of your
add-on is specified in its install manifest and uniquely
identifies it. You cannot change your UUID once it has been
submitted.{%- endtrans %}">?</span>
</th>
<td>
{{ addon.guid }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>

Просмотреть файл

@ -0,0 +1,106 @@
{% from "devhub/includes/macros.html" import tip, empty_unless %}
<form method="post" action="{{ url('devhub.addons.section', addon.id, 'basic', 'edit') }}">
<h3>
{{ _('Basic Information') }}
{% if not editable %}
<a href="{{ url('devhub.addons.section', addon.id, 'basic', 'edit') }}" class="button">
{{ _('Edit') }}</a>
{% endif %}
</h3>
<div class="item">
<div class="item_wrapper">
<table>
{# L10n: {0} is the addon name #}
<caption>{{ _('Basic Information for {0}')|f(addon.name) }}</caption>
<tbody>
<tr>
<th>{{ _("Name") }}</th>
<td>
{% if editable %}
{{ form.name|safe }}
{{ form.name.errors|safe }}
{% else %}
{{ addon.name }}
{% endif %}
</td>
</tr>
<tr>
<th>
{{ tip(_("Add-on URL"),
_("Choose a short, unique URL slug for your add-on.")) }}
</th>
<td id="slug_edit">
{% if editable %}
<span>{{ settings.SITE_URL }}/&hellip;/</span>{{ form.slug|safe }}
{{ form.slug.errors|safe }}
{% else %}
<a href="{{ addon.get_url_path() }}">
{{ settings.SITE_URL }}/&hellip;/{{ addon.slug }}</a>
{% endif %}
</td>
</tr>
<tr>
<th>
{{ tip(_("Summary"),
_("A short explanation of your add-on's basic functionality
that is displayed in search and browse listings, as well as
at the top of your add-on's details page.")) }}
</th>
<td>
{% if editable %}
{{ form.summary|safe }}
{{ form.summary.errors|safe }}
{% else %}
{{ addon.summary|nl2br }}
{% endif %}
</td>
</tr>
<tr>
<th>
{{ tip(_("Categories"),
_("Categories are the primary way users browse through add-ons.
Choose any that fit your add-on's functionality for the most
exposure.")) }}
</th>
<td id="addon_categories_edit">
{% if editable %}
{# TODO(gkoberger): Add categories #}
<strong>Coming Soon</strong>
{% else %}
{{ addon.categories.all()|join(' &middot; ')|safe }}
{% endif %}
</td>
</tr>
<tr>
<th>
{{ tip(_("Tags"),
_("Tags help users find your add-on and should be short
descriptors such as tabs, toolbar, or twitter. You
may have a maximum of {0} tags.").format(amo.MAX_TAGS)) }}
</th>
<td>
{% if editable %}
{# TODO(gkoberger): Add tags #}
<strong>Coming Soon</strong>
{% else %}
{% call empty_unless(tags) %}
{{ tags|join(', ') }}
{% endcall %}
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
{% if editable %}
<div class="listing-footer">
<button type="submit">{{ _('Save Changes') }}</button> {{ _('or') }}
<a href="{{ url('devhub.addons.section', addon.id, 'basic') }}"
class="addon-edit-cancel">
{{ _('Cancel') }}</a>
</div>
{% endif %}
</div>
</form>

Просмотреть файл

@ -0,0 +1,29 @@
{% macro tip(name, tip) %}
<span class="label">{{ name }}</span>
<span class="help tooltip" title="{{ tip }}">?</span>
{% endmacro %}
{% macro empty_unless(truthy) %}
{% if truthy %}
{{ caller() }}
{% else %}
<span class="empty">{{ _('None') }}</span>
{% endif %}
{% endmacro %}
{% macro flags(text, element, editable, alt_text=False) %}
{% if editable %}
<div>
{{ element|safe }}
{{ element.errors|safe }}
<label for="id_{{ element.html_name }}">{{ text }}
</div>
{% elif element %}
{{ text }}
{% elif alt_text %}
{# This is what's shown if the element is false #}
{# Defaults to nothing #}
{{ alt_text }}
{% endif %}
{% endmacro %}

Просмотреть файл

@ -525,7 +525,6 @@ class TestEditPayments(test_utils.TestCase):
eq_(ContribForm.initial(self.addon)['annoying'], amo.CONTRIB_AFTER)
class TestDisablePayments(test_utils.TestCase):
fixtures = ['base/apps', 'base/users', 'base/addon_3615']
@ -553,10 +552,19 @@ class TestDisablePayments(test_utils.TestCase):
class TestEdit(test_utils.TestCase):
fixtures = ['base/apps', 'base/users', 'base/addon_3615']
fixtures = ['base/apps', 'base/users', 'base/addon_3615',
'base/addon_5579']
def setUp(self):
super(TestEdit, self).setUp()
self.addon = self.get_addon()
assert self.client.login(username='del@icio.us', password='password')
self.url = reverse('devhub.addons.edit', args=[self.addon.id])
self.url_section = reverse('devhub.addons.section',
args=[self.addon.id, 'basic', 'edit'])
def get_addon(self):
return Addon.objects.no_cache().get(id=3615)
def test_redirect(self):
# /addon/:id => /addon/:id/edit
@ -564,6 +572,46 @@ class TestEdit(test_utils.TestCase):
url = reverse('devhub.addons.edit', args=[3615])
self.assertRedirects(r, url, 301)
def test_edit_basic(self):
old_name = self.addon.name
data = dict(name='new name',
slug='test_addon',
summary='new summary')
r = self.client.post(self.url_section, data)
eq_(r.status_code, 200)
addon = self.get_addon()
eq_(unicode(addon.name), data['name'])
eq_(addon.name.id, old_name.id)
eq_(unicode(addon.slug), data['slug'])
eq_(unicode(addon.summary), data['summary'])
def test_edit_basic_slugs_unique(self):
Addon.objects.get(id=5579).update(slug='test_slug')
data = dict(name='new name',
slug='test_slug',
summary='new summary')
r = self.client.post(self.url_section, data)
eq_(r.status_code, 200)
self.assertFormError(r, 'form', 'slug', 'This slug is already in use.')
def test_edit_basic_name_not_empty(self):
data = dict(name='',
slug=self.addon.slug,
summary=self.addon.summary)
r = self.client.post(self.url_section, data)
eq_(r.status_code, 200)
self.assertFormError(r, 'form', 'name', 'This field is required.')
class TestProfile(test_utils.TestCase):
fixtures = ['base/apps', 'base/users', 'base/addon_3615']

Просмотреть файл

@ -17,7 +17,8 @@ detail_patterns = patterns('',
url('^payments/disable$', views.disable_payments,
name='devhub.addons.payments.disable'),
url('^profile$', views.profile, name='devhub.addons.profile'),
)
url('^edit_(?P<section>[^/]+)(?:/(?P<editable>[^/]+))?$',
views.addons_section, name='devhub.addons.section'))
urlpatterns = decorate(write, patterns('',
url('^$', views.index, name='devhub.index'),
@ -25,7 +26,8 @@ urlpatterns = decorate(write, patterns('',
# URLs for a single add-on.
('^addon/(?P<addon_id>\d+)/', include(detail_patterns)),
# Redirect people who have /addons/ instead of /addon/.
('^addons/\d+/.*', lambda r: redirect(r.path.replace('addons', 'addon', 1))),
('^addons/\d+/.*',
lambda r: redirect(r.path.replace('addons', 'addon', 1))),
# Redirect to /addons/ at the base.
url('^addon$', lambda r: redirect('devhub.addons', permanent=True)),
@ -34,5 +36,4 @@ urlpatterns = decorate(write, patterns('',
name='devhub.addons.activity'),
url('^upload$', views.upload, name='devhub.upload'),
url('^upload/([^/]+)$', views.upload_detail,
name='devhub.upload_detail'),
))
name='devhub.upload_detail')))

Просмотреть файл

@ -4,6 +4,7 @@ import uuid
from django import http
from django.conf import settings
from django.shortcuts import get_object_or_404, redirect
from django.utils.translation import trans_real as translation
import commonware.log
import jingo
@ -13,6 +14,7 @@ from tower import ugettext_lazy as _lazy
import amo.utils
from amo.decorators import login_required, post_required
from access import acl
from addons.forms import AddonFormBasic
from addons.models import Addon, AddonUser, AddonLog
from addons.views import BaseFilter
from files.models import FileUpload
@ -90,15 +92,13 @@ def activity(request):
@dev_required
def edit(request, addon_id, addon):
tags_dev, tags_user = addon.tags_partitioned_by_developer
data = {
'page': 'edit',
'addon': addon,
'tags_user': [tag.tag_text for tag in tags_dev],
'tags_dev': [tag.tag_text for tag in tags_user],
'previews': addon.previews.all(),
}
'page': 'edit',
'addon': addon,
'tags': addon.tags.not_blacklisted().values_list('tag_text', flat=True),
'previews': addon.previews.all(),
}
return jingo.render(request, 'devhub/addons/edit.html', data)
@ -202,6 +202,7 @@ def profile(request, addon_id, addon):
return jingo.render(request, 'devhub/addons/profile.html',
dict(addon=addon, profile_form=profile_form))
def upload(request):
if request.method == 'POST' and 'upload' in request.FILES:
upload = request.FILES['upload']
@ -228,3 +229,38 @@ def upload_detail(request, uuid):
upload = get_object_or_404(FileUpload.uncached, uuid=uuid)
return jingo.render(request, 'devhub/validation.html',
dict(upload=upload))
@dev_required
def addons_section(request, addon_id, addon, section, editable=False):
models = {'basic': AddonFormBasic}
if section not in models:
return http.HttpResponseNotFound()
if editable:
if request.method == 'POST':
form = models[section](request.POST, request.FILES,
instance=addon)
if form.is_valid():
addon = form.save(addon)
editable = False
AddonLog.log(models[section], request, addon=addon,
action='edit '+section)
else:
form = models[section](instance=addon)
else:
form = False
tags = addon.tags.not_blacklisted().values_list('tag_text', flat=True)
data = {'addon': addon,
'form': form,
'lang': lang,
'editable': editable,
'tags': tags}
return jingo.render(request,
'devhub/includes/addon_edit_%s.html' % section, data)

Просмотреть файл

@ -131,7 +131,6 @@
/** This is so they match up size-wise **/
}
#edit-addon label {
font-weight: normal;
}

Просмотреть файл

@ -59,6 +59,7 @@ header hgroup h3 {
ul.errorlist {
margin-bottom: 0;
color: #c00;
clear:both;
}
.html-ltr a.more-link:after {
@ -759,8 +760,6 @@ div.popup-shim {
#tooltip span {
display: block;
line-height: 1.2em;
max-height: 3.8em;
overflow: hidden;
}
#tooltip:before {
@ -2632,12 +2631,6 @@ label > .optional {
margin: 0;
}
#slug_edit {
display: none;
}
#slug_value {
font-weight: bold;
}
#ajax_collections_list li {
cursor: pointer;
padding-left: 30px;

Просмотреть файл

@ -6,6 +6,25 @@ $(document).ready(function() {
return {pointTo: $(obj.click_target)};
}
});
$('#edit-addon').delegate('h3 a', 'click', function(e){
e.preventDefault();
parent_div = $(this).closest('.edit-addon-section');
a = $(this);
(function(parent_div, a){
parent_div.load($(a).attr('href'), addonFormSubmit);
})(parent_div, a);
return false;
});
$('.addon-edit-cancel').live('click', function(){
parent_div = $(this).closest('.edit-addon-section');
parent_div.load($(this).attr('href'));
return false;
});
});
function addonFormSubmit() {