Merge branch 'master' into generic_id

pull/1/head
eric 2012-01-07 17:46:29 -05:00
commit eeb594588e
45 changed files with 1590 additions and 317 deletions

View File

@ -57,6 +57,12 @@ class UserProfileAdmin(ModelAdmin):
class TransactionAdmin(ModelAdmin):
date_hierarchy = 'date_created'
class PaymentResponseAdmin(ModelAdmin):
pass
class ReceiverAdmin(ModelAdmin):
ordering = ('email',)
admin_site = RegluitAdmin("Admin")
admin_site.register(models.User, UserAdmin)
@ -72,3 +78,5 @@ admin_site.register(models.Ebook, EbookAdmin)
admin_site.register(models.Wishlist, WishlistAdmin)
admin_site.register(models.UserProfile, UserProfileAdmin)
admin_site.register(payment.models.Transaction, TransactionAdmin)
admin_site.register(payment.models.PaymentResponse, PaymentResponseAdmin)
admin_site.register(payment.models.Receiver, ReceiverAdmin)

View File

@ -92,6 +92,7 @@ class Campaign(models.Model):
amazon_receiver = models.CharField(max_length=100, blank=True)
work = models.ForeignKey("Work", related_name="campaigns", null=False)
managers = models.ManyToManyField(User, related_name="campaigns", null=False)
# status: INITIALIZED, ACTIVE, SUSPENDED, WITHDRAWN, SUCCESSFUL, UNSUCCESSFUL
status = models.CharField(max_length=15, null=True, blank=False, default="INITIALIZED")
problems = []
@ -149,9 +150,10 @@ class Campaign(models.Model):
else:
return 0
def transactions(self, pledged=True, authorized=True):
def transactions(self, summary=False, pledged=True, authorized=True, incomplete=True, completed=True):
p = PaymentManager()
return p.query_campaign(campaign=self, summary=False, pledged=pledged, authorized=authorized)
return p.query_campaign(campaign=self, summary=summary, pledged=pledged, authorized=authorized, incomplete=incomplete,
completed=completed)
def activate(self):
status = self.status

View File

@ -6,8 +6,10 @@ WSGISocketPrefix /opt/regluit
ServerName please.unglueit.com
ServerAdmin info@gluejar.com
Redirect 301 /admin https://please.unglueit.com/admin
Redirect 301 /accounts https://please.unglueit.com/accounts
RewriteEngine On
RewriteRule ^/$ https://please.unglueit.com/ [R=301]
RewriteRule /admin(.*) https://please.unglueit.com/admin$1 [R=301]
RewriteRule /accounts(.*) https://please.unglueit.com/accounts$1 [R=301]
WSGIDaemonProcess regluit processes=4 threads=4 python-eggs=/tmp/regluit-python-eggs
WSGIScriptAlias / /opt/regluit/deploy/regluit.wsgi

View File

@ -74,7 +74,7 @@ class UserData(forms.Form):
label=_("New Username"),
max_length=30,
regex=r'^[\w.@+-]+$',
help_text = _("30 characters or less."),
help_text = _("30 characters or fewer."),
error_messages = {
'invalid': _("This value may contain only letters, numbers and @/./+/-/_ characters.")
}

View File

@ -73,7 +73,7 @@
<li><a href="/stub/about">About</a></li>
<li><a href="http://www.gluejar.com/Blog">Blog</a></li>
<li><a href="/stub/press">Press</a></li>
<li><a href="/stub/newsletter">Newsletter</a></li>
<li><a href="http://eepurl.com/fKLfI">Newsletter</a></li>
</ul>
</div>
<div class="column">
@ -93,8 +93,8 @@
<div class="column">
<span>Help</span>
<ul>
<li><a href="{{faqurl}}">FAQ</a></li>
<li><a href="/stub/rhfaq">Rights Holder FAQ</a></li>
<li><a href="{{faqurl}}">General FAQ</a></li>
<li><a href="/faq/rightsholders/">Rights Holder FAQ</a></li>
<li><a href="{% url api_help %}">API</a></li>
<li><a href="mailto:support@gluejar.com">support@gluejar.com</a>
</ul>

View File

@ -1,6 +1,10 @@
{% extends "base.html" %}
{% block extra_head %}
<link href="/static/css/documentation.css" rel="stylesheet" type="text/css" />
{% block extra_js %}
<script type="text/javascript" src="/static/js/definitions.js"></script>
{% endblock %}
{% block extra_extra_head %}
<!-- extra head content in descendants goes in extra_extra_head, not extra_head, to avoid overwriting the documentation.css include -->
{% endblock %}
@ -9,38 +13,14 @@
{% block title %}{% endblock %}
{% block topsection %}
<div id="js-topsection">
<div class="js-main">
<div class="js-topnews">
<div class="user-block">
<div class="user-block1">
<div class="block-inner">
<div class="block-intro-text">With your help we raise money to buy book rights. The <span class="typo">unglued</span> books are free to download, here.</div>
<a class="my-setting readon"><span>Learn more</span></a>
</div>
</div>
<div class="user-block2">
<div class="block-inner">
<label class="title">Spread the Word</label>
<a href="https://www.facebook.com/sharer.php?u={{request.build_absolute_uri}}{{ request.path|urlencode:"" }}"><img src="/static/images/icons/facebook.png" alt="Facebook" title="Facebook" /></a>
<a href="https://twitter.com/intent/tweet?url={{request.build_absolute_uri}}{{ request.path|urlencode:"" }}&text=Unglue%ebooks%21"><img src="/static/images/icons/twitter.png" alt="Twitter" title="Twitter" /></a>
<a href="#"><img src="/static/images/icons/email.png" alt="email" title="email" /></a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% include "learn_more.html" %}
{% endblock %}
{% block content %}
<div id="main-container">
<div class="js-main">
<div id="js-leftcol">
{% include "explore.html" %}
{% include "faqmenu.html" %}
</div>
<div id="js-maincol-fr" class="have-right doc">
<div class="js-maincol-inner">

View File

@ -121,7 +121,6 @@
<span class="booklist-status-label">Status:&nbsp;</span><span class="booklist-status-text">{{ status }}</span>
</div>
<div class="listview panelfront side1 icons">
{% if fromsupport %}
{% if status == 'No campaign yet' or status == 'INITIALIZED' %}
<span class="rounded"><span class="grey"><span class="panelnope">Wished by&nbsp;</span>{{ work.wished_by.count }}</span></span>
{% else %}
@ -130,13 +129,6 @@
</div>
<div class="booklist-status-label">{{ work.percent_unglued_number }}%</div>
{% endif %}
{% else %}
<div class="booklist-status-img">
<img src="/static/images/images/icon-book-37by25-{{ work.percent_unglued }}.png" title="book list status" alt="book list status" />
</div>
<div class="booklist-status-label">{{ work.percent_unglued_number }}%</div>
{% endif %}
<div class="right_add"><img src="/static/images/book-panel/add_gray.png" alt="Add"/></div>
</div>
<div class="listview panelfront side1 ebooks">
{% if work.first_epub_url %}

View File

@ -9,7 +9,7 @@
<img class="user-avatar" src="/static/images/header/avatar.png" height="50" width="50" alt="Generic Ungluer Avatar" title="Ungluer" />
{% endif %}
</div>
<span class="comment_username">{{comment.user.username }}</span></a> <span>({{ comment.submit_date }})</span>: <span class="comment">{{ comment.comment }}</span>
<span class="comment_username">{{comment.user.username }}</span></a> <span>({{ comment.submit_date }})</span> <br /><span class="comment">{{ comment.comment }}</span>
</div>
{% endfor %}
</div>

View File

@ -2,24 +2,140 @@
{% block title %} FAQ {% endblock %}
{% block doccontent %}
<h2>FAQ</h2>
<h2>Frequently Asked Questions</h2>
<dl>
<dt style="background: #8dc63f">Help! My question isn't covered in the FAQs!</dt>
<dd>Please email us at <a href="mailto:support@gluejar.com">support@gluejar.com</a>. Especially during our alpha phase, we want to make sure everything on the site runs as smoothly as possible. Thanks for helping us do that.</dd>
{% if location == 'basics' or location == 'faq' %}
<h3>Basics</h3>
{% if sublocation == 'howitworks' or sublocation == 'all' %}
<h4>How It Works</h4>
<dt>What is Unglue.It?</dt>
<dd>TBA</dd>
<dt>What is Crowdfunding?</dt>
<dd>Crowdfunding is working together to support something you believe in. By pooling donations, big and small, from all over the world, we can make huge things happen.</dd>
<dt>What does it cost?</dt>
<dd>Unglue.It is free to join. Most of the things you can do here -- discovering books, adding them them to your wishlist, commenting, sharing -- are free too. If you choose to support a campaign, you may pledge whatever amount you're comfortable with.<br /><br />
If you're a rights holder, starting campaigns is free, too. You only pay Unglue.It if your campaign succeeds. For the basics on campaigns, see the FAQ on <a href="/faq/campaigns/">Campaigns</a>; for more details, see the <a href="/faq/rightsholders/">FAQ for Rights Holders</a>.</dd>
<dt>Who can use Unglue.It?</dt>
<dd>Anyone! We're located in the United States, but we welcome participants from all over the world.<br /><br />
To fund a campaign, you'll need a valid credit card. To start a campaign, you'll need to establish yourself with us as a rights holder, including demonstrating that you have the rights to the content you want to unglue. See the FAQs <a href="/faq/rightsholders/">for Rights Holders</a> for more.</dd>
<dt>Does Unglue.It own the copyright of unglued books?</dt>
<dd>No. When you unglue a book, the copyright stays with its current owner.<br /><br />
Ungluing involves licensing rights, just like traditional publishing transactions. We use <a href="http://creativecommons.org">Creative Commons</a> licenses to make ebooks available to the world while respecting and protecting copyright.<br /><br />
If you are a copyright holder, you will retain your copyright when you unglue a book. CC licenses are non-exclusive, so you also retain the right to enter into separate licensing agreements. You can read more about these licenses at the <a href="http://wiki.creativecommons.org/Frequently_Asked_Questions">Creative Commons FAQ</a>.</dd>
<dt>If I'm a rights holder and I unglue my book, does that mean I can never make money from it again?</dt>
<dd>No! You are free to enter into additional licensing agreements for other, non-unglued, editions of the work, including translation and film rights. You may continue to sell both print and ebook editions. You may use your unglued books as free samples to market your other works -- for instance, later works in the same series. You can use them to attract fans who may be interested in your speaking engagements, merchandise, or other materials. You absolutely may continue to profit from ungluing books -- and we hope you do!<br /><br />
For some examples of how authors and publishers have made free ebooks work for their business plans, see (TBA: with a little help, o'reilly, baen, ...?) If unglued books are working for you, we'd love to hear your story. Tell us at <a href="mailto:support@gluejar.com">support@gluejar.com</a>.</dd>
<dt>Why do rights holders want to unglue their books?</dt>
<dd>Lots of reasons! Unglued ebooks may be part of a marketing strategy to publicize other books or increase the value of an author's brand. Or they may be books that are no longer selling through conventional channels, and ungluing them is a low-risk way to get some extra value out of them. Or ungluing provides a simple digital strategy that pays for itself. Or the books may have been written to advance a cause, add to the public conversation, or increase human knowledge. These books succeed by having as many readers as possible.</dd>
<dt>I know a book that should be unglued.</dt>
<dd>Great! Find it in our database (using the search box above) and add it to your wishlist, so we know it should be on our list, too. You can also contact us: <!-- info? --></dd>
<dd>Great! Find it in our database (using the search box above) and add it to your wishlist, so we know it should be on our list, too. You can also contact us at <a href="mailto:rights@gluejar.com">rights@gluejar.com</a></dd>
<dt>I know a book that should be unglued, and I own its electronic rights.</dt>
<dd>Fabulous! Please refer to the <a href="/stub/rhfaq">Rights Holder FAQ</a> and then contact us.</dd><!-- which contact info? -->
<dd>Fabulous! Please refer to the FAQ <a href="/faq/rightsholders/">for Rights Holders</a> and then contact us at <a href="mailto:rights@gluejar.com">rights@gluejar.com</a>.</dd>
<dt>How much does a book cost?</dt>
<dt>Is there a widget that can be put on my own site to share my favorite books or campaigns?</dt>
<dd>The author or publisher set a price for giving the book to the world. Once you and your fellow ungluers raise enough money to meet that price, the Unglued ebook is available at no charge, for everyone, everywhere!</dd>
<dd>Yes! Every book page has an "Embed" option. Click that and copy/paste the HTML code wherever you like.</dd>
{% endif %}
{% if sublocation == 'account' or sublocation == 'all' %}
<h4>Your Account</h4>
<dt>I forgot my password. What do I do?</dt>
<dt>Are the unglued ebooks compatible with my reader?</dt>
There's a <a href="/accounts/password/reset/">forgot your password</a> link at the bottom of the <a href="/accounts/login/">sign in page</a>. Enter the email address you use for your account and we'll send you an email to help you reset your password.
<dd>Unglued ebooks are distributed with NO DRM, so they'll work on Kindle, iPad, Kobo, Mac, Windows, Linux... you get the idea. And if the ePub format isn't best for your device, you're free to shift unglued books to a format that works better for you.</dd>
<dt>I never received a confirmation email after I signed up. What do I do?</dt>
<dd>TBA</dd>
<dt>How do I delete my account?</dt>
<dd>TBA</dd>
<dt>How do I connect my Twitter, Facebook, GoodReads, and/or LibraryThing accounts? How can I link to my personal web site?</dt>
Click on "My Settings" near the top of your user profile page. (If you're logged in, your profile page is <a href="/">here</a>.) Some new options will appear. You can enter your personal URL and your LibraryThing username here. There are one-click options to connect your Twitter, Facebook, and GoodReads accounts. (You'll be asked to log in to those accounts, if you aren't logged in already.) Make sure to click on "Save Settings".
<dt>Why should I connect those accounts?</dt>
<dd>If you connect your Facebook or Twitter accounts, we'll use your user pics from those sites for your avatar here. If you connect LibraryThing or GoodReads, you can import your books there onto your wishlist here, using the My Settings area.</dd>
<dt>I don't want to connect my accounts. Do I have to?</dt>
<dd>Nope.</dd>
<dt>How can I change my profile name or password?</dt>
<dd>At the bottom of every page there's a link to an <a href="/accounts/edit/">account editing page</a> where you can change your profile name and password.</dd>
<dt>How can I change the email address associated with my account?</dt>
<dd>TBA</dd>
<dt>How do I unsubscribe from or adjust which emails I receive?</dt>
<dd>TBA<br /><br />
If you receive our newsletter, there's a link at the bottom of every message to manage your preferences. If you don't and would like to, you can <a href="http://eepurl.com/fKLfI">sign up here</a>.
</dd>
<dt>Is my personal information shared?</dt>
<dd>Short version: no. You may share information with Rights Holders so they can deliver premiums to you, but this is not required. For the long version, please read our <a href="/privacy/">privacy policy</a>.
{% endif %}
{% if sublocation == 'company' or sublocation == 'all' %}
<h4>The Company</h4>
<dt>How can I contact Unglue.It?</dt>
<dd>For support requests, <a href="mailto:support@gluejar.com">support@gluejar.com</a>. For general inquiries, use our Ask Questions Frequently account, <a href="mailto:aqf@gluejar.com">aqf@gluejar.com</a>. For rights inquiries, <a href="mailto:rights@gluejar.com">rights@gluejar.com</a>.</dd>
<dt>Who is Unglue.It?</dt>
<dd>We are <a href="http://go-to-hellman.blogspot.com">Eric Hellman</a>, <a href="http://www.ameckeco.com/">Amanda Mecke</a>, <a href="http://raymondyee.net/">Raymond Yee</a>, and <a href="http://andromedayelton.com">Andromeda Yelton</a>. (TBA: include contractors?) We come from the worlds of entrepreneurship, linked data, physics, publishing, education, and library science, to name a few. You can learn more about us at our personal home pages (linked above) or <a href="http://gluejar.com/team">the team page</a> of our corporate site.</dd>
<dt>Are you a non-profit company?</dt>
<dd>No. Gluejar is a for-profit company with a public-spirited mission. We work with both non-profit and commercial partners.</dd>
<dt>Why does Unglue.It exist?</dt>
<dd>TBA</dd>
{% endif %}
{% endif %}
{% if location == 'campaigns' or location == 'faq' %}
<h3>Campaigns</h3>
{% if sublocation == 'overview' or sublocation == 'all' %}
<h4>Overview</h4>
{% comment %}
These need to be put in proper order. Also this should be broken down into the general FAQ and the RH FAQ; only the basic create/manage campaign, and the supporter mechanisms of interacting with campaigns, should be in the basic FAQ; more technical questions about running campaigns should be in the RH FAQ.
<dt>Why does it say a book's campaign has been suspended or withdrawn?</dt>
@ -30,11 +146,376 @@
<dd>Your pledge will time out according to its original time limit. If the campaign is resolved and reactivated before your pledge has timed out, your pledge will become active again. If the campaign is not reactivated before your pledge's time limit, your pledge will expire and you will not be charged. As always, you will only be charged if a campaign is successful, within its original time limit.</dd>
<dt>What if I want to change or cancel a pledge?</dt>
{% endcomment %}
<dt>Who is eligible to start a campaign on Unglue.It?</dt>
<dd>To start a campaign, you need to be a verified rights holder who has signed a Platform Services Agreement with us. If you hold the electronic rights for one or more works, please contact <a href="mailto:rights@gluejar.com">rights@gluejar.com</a> to start the process.</dd>
<dt>How can I claim a work for which I am the rights holder?</dt>
<dd>On every book page there is a Details tab. If you have a signed Platform Services Agreement on file, one of the options on the Details tab will be "Claim This Work". If you represent more than one rights holder, choose the correct one for this work and click "Claim".<br /><br />If you expect to see that and do not, either we do not have a PSA from you yet, or we have not yet verified and filed it. Please contact us at <a href="mailto:rights@gluejar.com">rights@gluejar.com</a>.</dd>
<dt>Why should I claim my works?</dt>
<dd>You need to claim a work before you will be able to start a campaign for it.</dd>
<dt>Can I have more than one campaign?</dt>
<dd>Yes!</dd>
<dt>Where can I find a campaign page?</dt>
<dd>If you're looking for a specific book, search for it. The book's page becomes a campaign page when its campaign starts. Your bookmarks and widgets for that book will still work.<br /><br />
If you want to find an interesting campaign and don't have a specific book in mind, see the Explore sidebar (on almost any page except the FAQs) for some of our favorites.</dd>
<h4>Campaign Pages</h4>
<dt>Can a campaign be edited after launching? (combine with changing funding goal/deadline?)</dt>
<dd>TBA</dd>
<dt>Can campaigns be edited after funding is completed?</dt>
<dd>TBA</dd>
<dt>What is a campaign page?</dt>
<dd>TBA</dd>
{% endif %}
{% if sublocation == 'funding' or sublocation == 'all' %}
<h4>Funding</h4>
<dt>Is a Paypal account required to launch a campaign?</dt>
<dd>TBA</dd>
<dt>Are a funding goal and deadline required?</dt>
<dd>Yes.</dd>
<dt>Can I change my funding goal?</dt>
<dd>TBA</dd>
<dt>Can I change my deadline?</dt>
<dd>TBA</dd>
<dt>Can funding be canceled?</dt>
<dd>TBA</dd>
<dt>Can I offer tax deductions to people who pledge to my campaign?</dt>
<dd>TBA</dd>
<dt>Are contributions refundable?</dt>
<dd>TBA</dd>
<dt>How do I know when I have started fundraising?</dt>
<dd>TBA</dd>
<dt>How do I collect money for my campaign? (include registering & verifying info as needed)</dt>
<dd>TBA</dd>
<dt>Where can I check the status of my funds?</dt>
<dd>TBA</dd>
<dt>What happens if my campaign reaches its funding goal before its deadline? Can campaigns raise more money than their goal?</dt>
<dd>TBA</dd>
<dt>Can people contribute anonymously?</dt>
<dd>We intend to implement this feature, but have not yet.</dd>
<dt>What happens if a supporter's credit card is declined?</dt>
Can campaigns raise more money than their goal?
<dt>What fees does Unglue.It charge?</dt>
<dd>TBA</dd>
<dt>Does Paypal charge any fees?</dt>
<dd>TBA</dd>
<dt>Does it cost money to start a campaign on Unglue.It?</dt>
<dd>No.</dd>
<dt>I'm having problems verifying my account with Paypal. What do I need to do?</dt>
<dd>TBA</dd>
<dt>Will Paypal send me a 1099-K tax form?</dt>
<dd>TBA</dd>
<dt>What happens to my pledge if a campaign does not succeed?</dt>
<dd>Your credit card will only be charged when campaigns succeed. If they do not, your pledge will expire and you will not be charged.</dd>
{% endif %}
{% if sublocation == 'premiums' or sublocation == 'all' %}
<h4>Premiums</h4>
<dt>What are premiums?</dt>
<dd>Premiums are bonuses people get for supporting a successful campaign, to thank them and incentivize them to pledge. If you've ever gotten a tote bag from NPR, you know what we're talking about.</dd>
<dt>Are premiums required?</dt>
<dd>Yes. All campaigns have a required set of premiums, as follows:<br /><br />
<ul>
<li>$1 // The unglued ebook delivered to your inbox.</li>
<li>$25 // Your name under "supporters" in the acknowledgements section.</li>
<li>$50 // Your name and link of your choice under "benefactors"</li>
<li>$100 // Your name, link of your choice, and a brief message (140 characters max) under "bibliophiles"</li>
</ul>
Rights holders are encouraged to offer additional premiums to engage supporters' interest. Think about things that uniquely represent yourself or your work, that are appealing, and that you can deliver quickly and within your budget.</dd>
<dt>What can be offered as a premium? What items are prohibited as premiums?</dt>
<dd>TBA</dd>
<dt>Who is responsible for making sure Rights Holders deliver premiums?</dt>
<dd>The rights holder. (TBA: is this correct? do we need to say more?</dd>
<dt>Who creates the premiums for each campaign?</dt>
<dd>Unglue.It determines the required premiums; the rights holder determines any additional premiums for a campaign.</dd>
<dt>Is there a minimum or maximum for how much a premium can cost?</dt>
<dd>TBA</dd>
<dt>How can I get my supporters' information (mailing address, T-shirt size, etc.) to fulfill premiums?</dt>
<dd>TBA</dd>
(TBA: questions about specifying estimated delivery dates/what the process timeline should be)
{% endif %}
{% endif %}
{% if location == 'unglued_ebooks' or location == 'faq' %}
<h3>Unglued Ebooks</h3>
{% if sublocation == 'general' or sublocation == 'all' %}
<h4>General Questions</h4>
<dt>How much does a book cost?</dt>
<dd>The author or publisher set a price for giving the book to the world. Once you and your fellow ungluers raise enough money to meet that price, the Unglued ebook is available at no charge, for everyone, everywhere!</dd>
<dt>Will Unglue.It have campaigns for all kinds of books?</dt>
<dd>Yes. You choose what books to wishlist and what books to support. Your passion will drive the campaigns. We aim to host campaigns for diverse subjects genres, from romance novels to poetry, from history to medicine, from the lectures of famous scentists to your favorite chapter book when you were 8 years old.</dd>
<dt>Will you raise money for books already for sale as an ebook?</dt>
<dd>Yes. Books available as ebooks can still be unglued, to make them available more easily to more people.</dd>
<dt>Does an unglued book have to be out-of-print?</dt>
<dd>No. Unglued books can be anything in copyright, whether 75 years or 7 days old, whether they sell in paperback or hardcover and or can only be found in used bookstores -- or nowhere.</dd>
<dt>Does Gluejar help to fund self-published books?</dt>
<dd>No.</dd><!-- should we provide pointers to other resources? encourage people to CC-license their own works?-->
<dd>No.</dd>{% comment %}should we provide pointers to other resources? encourage people to CC-license their own works?{% endcomment %}
{% endif %}
{% if sublocation == 'using' or sublocation == 'all' %}
<h4>Using Your Unglued Ebook</h4>
<dt>What can I do, legally, with an unglued ebook? What can I NOT do?</dt>
<dd>Unless otherwise specified for specific books, unglued ebooks are released under a (TBA:link)Creative Commons Attribution-NonCommercial-NoDerivatives license (CC BY-NC-ND). (TBA: improve with input from CC site)<br /><br />
This means that you <b>can</b>: make copies; keep them for as long as you like; shift them to other formats (like .mobi or PDF); share them with friends or on the internet; download them for free.<br /><br />
You <b>cannot</b>: sell them, or otherwise use them commercially, without permission from the rights holder; make derivative works, such as translations or movies, without permission from the rights holder; remove the author's name from the book, or otherwise pass it off as someone else's work; or remove or change the CC license.</dd>
<dt>Are the unglued ebooks compatible with my device? Do I need to own an ereader, like a Kindle or a Nook, to read them?</dt>
<dd>Unglued ebooks are distributed with NO DRM, so they'll work on Kindle, iPad, Kobo, Nook, Android, Mac, Windows, Linux... you get the idea. Whether you have an ereader, a tablet, a desktop or laptop computer, or a smartphone, there are reading apps that work for you. And if the ePub format isn't best for your device, you're free to shift unglued books to a format that works better for you. (TBA: I think ePub does NOT work on Kindle so we need to address that, & how people can deal)</dd>
<dt>Do I need to have a library card to read an unglued ebook?</dt>
<dd>No. (Though we hope you have a library card anyway!) While your library may make unglued ebooks available, they will be available from other sources as well.</dd>
<dt>How long do I have to read my unglued book? When does it expire?</dt>
<dd>It doesn't! You may keep it as long as you like. There's no need to return it, and it won't stop working after some deadline.</dd>
<dt>I love my unglued ebook and I want to loan it to my friends. Can I?</dt>
<dd>Yes! Because everything with a <a href="http://creativecommons.org">Creative Commons</a> license is free to copy and share, you can copy your file and give it to anyone (and everyone) you want. Just don't sell it -- unglued ebooks are only licensed for noncommercial use.</dd>
<dt>Will I be able to dowload an unglued ebook directly from Unglue.It?</dt>
<dd>Unglue.It will provide links to places where you can find unglued and public domain ebooks. We will not host them ourselves. We encourage you to look for them at bookstores, libraries, literary blogs, social reading sites, and all kinds of places people go to read and talk about books.</dd>
{% endif %}
{% if sublocation == 'copyright' or sublocation == 'all' %}
<h4>Ungluing and Copyright</h4>
<dt>Why does an unglued book have to be in copyright?</dt>
<dd>Because books out of copyright are already free for you to copy, remix, and share! If a book is in the (TBA: link) public domain, it's already unglued.</dd>
<dt>How can I tell if a book is in copyright or not?</dt>
<dd>Unfortunately, it can be complicated -- which is why Unglue.It wants to simplify things, by making unglued ebooks unambiguously legal to use in certain ways. The laws governing copyright and the public domain vary by country, so a book can be copyrighted in one place and not in another. (TBA: link, library copyright slider; other?) In the United States, the Library Copyright Slider gives a quick estimate. (TBA: more links for people who want to learn more?)<br /><br />
Unglue.It signs agreements concerning the copyright status of every work we unglue, so you can be confident when reading and sharing an unglued ebook.</dd>
{% endif %}
{% endif %}
{% if location == 'rightsholders' %}
<h3>For Rights Holders</h3>
{% if sublocation == 'authorization' or sublocation == 'all' %}
<h4>Becoming Authorized</h4>
<dt>What do I need to do to become an authorized Rights Holder on Unglue.it?</dt>
<dd>Contact Amanda Mecke, <a href="mailto:amecke@gluejar.com">amecke@gluejar.com</a>, to discuss signing our Platform Services Agreement. This is the first step in being able to make an offer to license, set a monetary goal, and run a campaign on Unglue.it.</dd>
<dt>Do I need to know the exact titles I might want to unglue before I sign the Platform Services Agreement?</dt>
<dd>No. You only need to agree to the terms under which you will use Unglue.it to raise money to release an ebook using the <a href="http://creativecommons.org">Creative Commons</a> license. You can decide which specific titles you wish to make available for licensing later. You can run campaigns for as many, or as few, titles at a time as you like.</dt>
{% endif %}
{% if sublocation == 'campaigns' or sublocation == 'all' %}
<h4>Launching Campaigns</h4>
<dt>What do I need to create a campaign?</dt>
<dd>TBA</dd>
<dt>How do I launch my first campaign?</dt>
<dd>TBA</dd>
<dt>Can I Unglue only one of my books? Can I unglue all of them?</dt>
<dd>Yes! It's entirely up to you. Each Campaign is for a individual title and a separate fundraising goal.</dd>
<dt>Can raise any amount of money I want?</dt>
<dd>You can set any goal that you want in a Campaign. Unglue.It cannot guarantee that a goal will be reached.</dd>
<dt>What should I include in my campaign page?</dt>
<dd>Show us why we should love your book. What makes it powerful, intriguing, moving, thought-provoking, important? How does it capture readers' imaginations, engage their minds, or connect to their emotions? Remind readers who loved the book why they loved it, and give people who haven't read it reasons they want to.<br /><br />
We strongly encourage you to include video. You can upload it to YouTube and embed it here. We also strongly encourage links to help readers explore further -- authors' home pages, organizations which endorse the book, positive reviews, et cetera. Think about blurbs and awards which showcase your book. But don't just write a catalog entry or a laundry list: be relatable, and be concise.</dd>
<dt>What should I ask my supporters to do?</dt>
<dd>(TBA: support campaign and share the word)</dd>
{% endif %}
{% if sublocation == 'publicity' or sublocation == 'all' %}
<h4>Publicizing Campaigns</h4>
<dt>I need help using social media channels to publicize my campaign.</dt>
<dd>We're developing a social media toolkit for rights holders. Please tell us what you think should be in it so we can serve you better: <a href="mailto:andromeda@gluejar.com">andromeda@gluejar.com</a>. In the meantime we're happy to help you one-on-one, at the same address.</dd>
<dt>Can I contact my supporters directly?</dt>
<dd>TBA</dd>
<dt>How do I get feedback from supporters or fans about my campaign?</dt>
<dd>TBA</dd>
<dt>How do I share my campaign?</dt>
<dd>(TBA: use widget, share functions on page, own social media networks, friends, their friends...)</dd>
<dt>How can I get my campaign featured on the home page or the Explore menu pages?</dt>
<dd>TBA</dd>
<dt>How can I get press coverage or social media buzz for my campaign?</dt>
<dd>We're developing a social media toolkit to help you with this; stay tuned. We're also happy to work with you one-on-one. Email <a href="mailto:support@gluejar.com">support@gluejar.com</a>.
{% endif %}
{% if sublocation == 'conversion' or sublocation == 'all' %}
<h4>Ebook Conversion</h4>
<dt>I am a copyright holder. Do I need to already have a digital file for a book I want to nominate for a pledge drive?</dt>
<dd>No. You may run campaigns for any of your books, even those that exist only in print copies or are out of print. Any print book can be scanned to create a digital file that can then become an ePub unglued ebook.</dd>
<dt>Will Unglue.It scan the books and produce ePub files?</dt>
<dd>No. We will help you find third parties who will contract for conversion services. (TBA: is this true?)</dd>
<dt>Will Unglue.It raise money to help pay for conversion costs?</dt>
<dd>Yes. Your campaign target should include conversion costs.</dd>
{% endif %}
{% if sublocation == 'rights' or sublocation == 'all' %}
<h4>Rights</h4>
<dt>If I am an author with my book still in print, can I still start a Campaign to unglue it?</dt>
<dd>This depends entirely on your original contract with your publisher. You should seek independent advice about your royalty clauses and the "Subsidiary Rights" clauses of your contract. The Authors' Guild provides guidance for its members.</dd>
<dt>If I am an author do I have to talk to my publisher or agent first?</dt>
<dd>It is your responsibility to get advice on the current status of any contracts you may have ever had for the right to publish your work, whether or not a book is in print now. <a href="http://creativecommons.org">Creative Commons</a> licenses are media neutral and worldwide (English). You may need waivers from other parties who have exclusive licenses for this book.</dd>
<dt>If I am a publisher, but I do not have an ebook royalty in my contract, can I sign your Platform Services Agreement?</dt>
<dd>We can't interpret your particular contract regarding subsidiary rights and your ability to use a Creative Commons license. Please seek qualified independent advice regarding the terms of your contract. In any event, you will also want the author to cooperate with you on a successful fundraising campaign, and you can work together to meet the warranties of the PSA.</dd>
<dt>Is the copyright holder the same as a Rights Holder?</dt>
<dd>Not necessarily. If you are the author and copyright holder but have a contract with a publisher, the publisher may be the authorized Rights Holder with respect to electronic rights, and they may be able to sign a licensing agreement on your behalf. Again, you must get advice about your individual contract, especially subsidiary rights clauses and exclusivity.</dd>
Questions about the Unglue.it license
<dt>Can I offer a book to be Unglued even if I cannot include all the illustrations from the print edition?</dt>
<dd>Yes. If permission to reprint cannot not be obtained for items such as photographs, drawings, color plates, as well as quotations from lyrics and poetry, we can produce an unglued edition which leaves them out. Please indicate the difference between the editions on your Campaign page.</dd>
<dt>What impact does ungluing a book have on the rights status of my other editions?</dt>
<dd>The Creative Commons license will apply only to the unglued edition, not to the print or any other editions. It does not affect the rights status of those other editions.</dd>
<dt>Can an Unglued Ebook be issued only in the US?</dt>
<dd>No. An Unglued Ebook is released to the world. It uses a <a href="http://creativecommons.org">Creative Commons</a> license which is territory-neutral. That is, the unglued ebook can be read by anyone, anywhere in the world, subject always to the non-commercial limitations of the license.</dd>
{% endif %}
<dt>What is my responsibility for answering questions from supporters and non-supporters?</dt>
<dd>TBA</dd>
<dt>If I am unable to complete my campaign as listed, what should I do?</dt>
<dd>TBA</dd>
</dl>
{% endif %}
{% endblock %}
{% comment %}
-- stuff that needs to be organized --
Will you tell me when campaigns I have pledged to succeed?
What does it mean when I wish for a book to be Unglued?
Can I share my wish list with others?
I see where I can comment on a campaign, but where can I talk about it with other readers?
See our links to Goodreads, Library Thing...etc where you can join conversations in progress.
-- stuff that will only go on the RH FAQ (if it's above, needs to be copy/pasted over to there)
Questions about becoming an authorized Rights Holder
Does a Creative Commons license mean the Rights Holders doesn't get paid?
No. An Unglued Ebook is pre-paid. A CC BY NC ND license means no one else can sell or adapt the ebook for profit. The Rights Holder can set as high a fee as wanted as a fundraising goal, instead of waiting decades for royalties.
<dt>How do I keep track of my contributors and the fulfillment of premiums?</dt>
<dd>TBA</dd>
{% endcomment %}

View File

@ -0,0 +1,46 @@
<div class="jsmodule">
<h3 class="jsmod-title"><span>FAQs</span></h3>
<div class="jsmod-content">
<ul class="menu level1">
<li class="first parent {% if location != 'basics' %}collapse{% endif %}">
<a href="/faq/basics/"><span>Basics</span></a>
<ul class="menu level2">
<li class="first"><a href="/faq/basics/howitworks"><span>How it Works</span></a></li>
<li><a href="/faq/basics/account"><span>Your Account</span></a></li>
<li class="last"><a href="/faq/basics/company/"><span>The Company</span></a></li>
</ul>
</li>
<li class="parent {% if location != 'campaigns' %}collapse{% endif %}">
<a href="/faq/campaigns/"><span>Campaigns</span></a>
<ul class="menu level2">
<li class="first"><a href="/faq/campaigns/overview"><span>Overview</span></a></li>
<li><a href="/faq/campaigns/funding"><span>Funding</span></a></li>
<li class="last"><a href="/faq/campaigns/premiums"><span>Premiums</span></a></li>
</ul>
</li>
<li class="parent {% if location != 'unglued_ebooks' %}collapse{% endif %}">
<a href="/faq/unglued_ebooks/"><span>Unglued Ebooks</span></a>
<ul class="menu level2">
<li class="first"><a href="/faq/unglued_ebooks/general"><span>General Questions</span></a></li>
<li><a href="/faq/unglued_ebooks/using"><span>Using Your Unglued Ebook</span></a></li>
<li class="last"><a href="/faq/unglued_ebooks/copyright"><span>Ungluing and Copyright</span></a></li>
</ul>
</li>
<li class="parent {% if location != 'rightsholders' %}collapse{% endif %}">
<a href="/faq/rightsholders/"><span>For Rights Holders</span></a>
<ul class="menu level2">
<li class="first"><a href="/faq/rightsholders/authorization"><span>Becoming Authorized</span></a></li>
<li><a href="/faq/rightsholders/campaigns"><span>Launching Campaigns</span></a></li>
<li><a href="/faq/rightsholders/publicity"><span>Publicizing Campaigns</span></a></li>
<li><a href="/faq/rightsholders/conversion"><span>Ebook Conversion</span></a></li>
<li class="last"><a href="/faq/rightsholders/rights/"><span>Rights</span></a></li>
</ul>
</li>
</ul>
</div>
</div>

View File

@ -10,29 +10,14 @@
<script src="/static/js/jquery-1.6.3.min.js"></script>
<!-- expands/collapses the learn more section -->
<script type="text/javascript">
var $j = jQuery.noConflict();
$j(document).ready(function(){
$j('.user-block-hide').hide();
$j('.user-block1 a').click(
function() {
$j(this).toggleClass("active");
$j(".user-block-hide").slideToggle(300);
$j("a.readon").toggleClass("down");
}
);
});
</script>
<!-- toggle to panelview state instead of listview default -->
<script type="application/x-javascript">
<script type="text/javascript">
jQuery(document).ready(function($) {
$('.listview').addClass("panelview").removeClass("listview");
});
</script>
<script type="text/javascript" src="/static/js/definitions.js"></script>
<script type="text/javascript" src="/static/js/greenpanel.js"></script>
<script src="/static/js/slides.min.jquery.js"></script>
@ -61,35 +46,7 @@ var $j = jQuery.noConflict();
{% endblock %}
{% block topsection %}
<div id="js-topsection">
<div class="js-main">
<div class="js-topnews">
<div class="user-block">
<div class="user-block1">
<div class="block-inner">
<div class="block-intro-text">With your help we raise money to buy book rights. The <span class="typo">unglued</span> books are free to download, here.</div>
<a class="my-setting readon"><span>Learn more</span></a>
</div>
</div>
<div class="user-block2">
<div class="block-inner">
<label class="title">Spread the Word</label>
<a href="https://www.facebook.com/sharer/sharer.php?src=bm&u={{request.build_absolute_uri}}"><img src="/static/images/supporter_icons/facebook_square.png" alt="Facebook" title="Facebook" /></a>
<a href="https://twitter.com/share"><img src="/static/images/supporter_icons/twitter_square.png" alt="tweeter" title="Twitter" /></a>
</div>
</div>
</div>
<div class="user-block-hide">
<div class="quicktour"><span class="highlight">We all have books we love so much, we'd like to give them to the world.</span> We want to share them, but also reward their creators. With digital books, it can be hard to do both.</div>
<div class="movingrightalong"></div>
<div class="quicktour"><span class="highlight">Unglue.it offers a win-win solution: Crowdfunding.</span> We run pledge campaigns for books; you chip in. When, together, we've reached the goal, we'll reward the book's creators and issue an unglued ebook.</div>
<div class="movingrightalong"></div>
<div class="quicktour last"><a href="https://creativecommons.org/">Creative Commons</a> licensing means everyone, everywhere can read and share the unglued book — freely and legally. <span class="highlight">You've given your favorite book to the world.</span></div>
</div>
</div>
</div>
</div>
{% include "learn_more.html" %}
{% endblock %}
{% block content %}
@ -200,7 +157,7 @@ var $j = jQuery.noConflict();
<ul class="ungluingwhat">
{% for event in events %}
<li>
<div class="user-avatar"><a href="{% url supporter event.wishlist.user.username %}"><img src="{% if event.wishlist.user.picurl %}{{ event.wishlist.user.picurl}}{% else %}/static/images/landingpage/user-avatar.png{% endif %}" width="43" height="43" title="{{event.wishlist.user.username}}" alt="{{event.wishlist.user.username}} avatar" /></a></div>
<div class="user-avatar"><a href="{% url supporter event.wishlist.user.username %}"><img src="{% if event.wishlist.user.picurl %}{{ event.wishlist.user.picurl}}{% else %}/static/images/header/avatar.png{% endif %}" width="43" height="43" title="{{event.wishlist.user.username}}" alt="{{event.wishlist.user.username}} avatar" /></a></div>
<div class="user-book-info">
<p class="user-book-info"><a href="{% url supporter event.wishlist.user.username %}">{{event.wishlist.user.username|truncatechars:20}}</a> is Wishing For</p>
<a class="user-book-name" href="{% url work event.work.id %}">{{ event.work.title }}</a>

View File

@ -0,0 +1,36 @@
<div id="js-topsection">
<div class="js-main">
<div class="js-topnews">
<div class="user-block">
<div class="user-block1">
<div class="block-inner">
<div class="block-intro-text">
<div><span class="def">unglue</span> (v. t.) 1. To pay an author or publisher in full, up front, for publishing a Creative Commons ebook.</div>
<div><span class="def">unglue</span> (v. t.) 2. To make a digital book free to read and use, worldwide.</div>
<div><span class="def">unglue</span> (v. t.) 3. To make it clearly legal for a digital book to be used, distributed, archived and preserved by libraries.</div>
<div><span class="def">unglue</span> (v. t.) 4. For an author or publisher, to accept a fixed amount of money from the public for its unlimited use of an ebook.</div>
<div><span class="def">unglue</span> (v. t.) 5. To give your favorite books to everyone on earth.</div>
<div><span class="def">unglue</span> (v. t.) 6. To reward authors and publishers for sharing books with the world.</div>
</div>
<a class="my-setting readon"><span>Learn more</span></a>
</div>
</div>
<div class="user-block2">
<div class="block-inner">
<label class="title">Spread the Word</label>
<a href="https://www.facebook.com/sharer/sharer.php?src=bm&u={{request.build_absolute_uri}}"><img src="/static/images/supporter_icons/facebook_square.png" alt="Facebook" title="Facebook" /></a>
<a href="https://twitter.com/share"><img src="/static/images/supporter_icons/twitter_square.png" alt="tweeter" title="Twitter" /></a>
</div>
</div>
</div>
<div class="user-block-hide">
<div class="quicktour"><span class="highlight">We all have books we love so much, we'd like to give them to the world.</span> We want to share them, but also reward their creators. With digital books, it can be hard to do both.</div>
<div class="movingrightalong"></div>
<div class="quicktour"><span class="highlight">Unglue.it offers a win-win solution: Crowdfunding.</span> We run pledge campaigns for books; you chip in. When, together, we've reached the goal, we'll reward the book's creators and issue an unglued ebook.</div>
<div class="movingrightalong"></div>
<div class="quicktour last"><a href="https://creativecommons.org/">Creative Commons</a> licensing means everyone, everywhere can read and share the unglued book — freely and legally. <span class="highlight">You've given your favorite book to the world.</span></div>
</div>
</div>
</div>
</div>

View File

@ -9,7 +9,7 @@
{% block doccontent %}
<h2>Campaign: {{campaign.name}}</h2>
<p>Wonderful: We're glad that you would like to support this campaign.</p>
<div class="thank-you">Thank you!</div>
<div class="book-detail">
<div class="book-detail-img"><a href="#">

View File

@ -2,8 +2,6 @@
{% block extra_css %}
<link rel="stylesheet" href="/static/css/book_list.css">
<link rel="stylesheet" href="/static/css/book_panel.css">
<!-- important! search.css goes last -->
<link rel="stylesheet" href="/static/css/search.css">
{% endblock %}
{% block extra_head %}
<script type="text/javascript" src="/static/js/wishlist.js"></script>
@ -43,8 +41,10 @@
{% for work in results %}
<div class="{% cycle 'row1' 'row2' %}">
{% with work.googlebooks_id as googlebooks_id %}
{% with work.last_campaign_status as status %}
{% with work.last_campaign.deadline as deadline %}
{% include "book_panel.html" %}
{% endwith %}
{% endwith %}{% endwith %}{% endwith %}
</div>
{% empty %}
<h2>Sorry, couldn't find that!</h2>

View File

@ -188,7 +188,7 @@ how do I integrate the your wishlist thing with the tabs thing?
<div id="loadlt"><input type="submit" value="Add your LibraryThing library" /></div>
</form>
{% else %}
<div id="loadlt"><div>Add your LibraryThing ID to import from LibraryThing: </div></div>
<div id="loadlt"><div>Add your LibraryThing ID to import from LibraryThing.</div></div>
{% endif %}
</div>
</div>

View File

@ -150,7 +150,7 @@ jQuery(document).ready(function(){
{{ claim.rights_holder.rights_holder_name }}
{% endif %}
{% endfor %}
has agreed to release <i>{{work.title}}</i> to the world as a Creative Commons licensed ebook if ungluers can join together to raise ${{ work.last_campaign.target }} by {{ work.last_campaign.deadline }}.
, has agreed to release <i>{{work.title}}</i> to the world as a Creative Commons licensed ebook if ungluers can join together to raise ${{ work.last_campaign.target }} by {{ work.last_campaign.deadline }}.
You can help!</p>
{{ work.last_campaign.description|safe }}
{% else %}
@ -169,7 +169,7 @@ jQuery(document).ready(function(){
<div id="tabs-3" class="tabs">
<div class="tabs-content">
{% for supporter in work.wished_by %}
<div class="work_supporter">
<div class="work_supporter_nocomment">
<a href="/supporter/{{supporter}}">
<div class="work_supporter_avatar">
{% if supporter.profile.pic_url %}

View File

@ -3,6 +3,11 @@ import re
from django.test import TestCase
from django.test.client import Client
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from regluit.core.models import Work, Campaign
from decimal import Decimal as D
import datetime
class WishlistTests(TestCase):
@ -67,3 +72,29 @@ class GoogleBooksTest(TestCase):
r = self.client.get("/googlebooks/wtPxGztYx-UC/")
self.assertEqual(r.status_code, 302)
self.assertEqual(r['location'], work_url)
class CampaignUiTests(TestCase):
def setUp(self):
self.user = User.objects.create_user('test', 'test@example.org', 'test')
self.client = Client()
# load a Work and a Campaign to create a Pledge page
self.work = Work(title="test Work")
self.work.save()
self.campaign = Campaign(target=D('1000.00'), deadline=datetime.datetime.utcnow() + datetime.timedelta(days=180),
work=self.work)
self.campaign.save()
self.campaign.activate()
def test_login_required_for_pledge(self):
""" Make sure that the user has to be logged in to be able to access a pledge page"""
pledge_path = reverse("pledge", kwargs={'work_id': self.work.id})
r = self.client.get(pledge_path)
self.assertEqual(r.status_code, 302)
# now login and see whether the pledge page is accessible
self.client.login(username='test', password='test')
r = self.client.get(pledge_path)
self.assertEqual(r.status_code, 200)
def tearDown(self):
pass

View File

@ -5,7 +5,7 @@ from django.views.generic import ListView, DetailView
from django.contrib.auth.decorators import login_required
from regluit.core.models import Campaign
from regluit.frontend.views import CampaignFormView, GoodreadsDisplayView, LibraryThingView, PledgeView
from regluit.frontend.views import CampaignFormView, GoodreadsDisplayView, LibraryThingView, PledgeView, FAQView
from regluit.frontend.views import CampaignListView, DonateView, WorkListView
urlpatterns = patterns(
@ -23,8 +23,9 @@ urlpatterns = patterns(
url(r"^rightsholders/claim/$", "claim", name="claim"),
url(r"^rh_admin/$", "rh_admin", name="rh_admin"),
url(r"^campaign_admin/$", "campaign_admin", name="campaign_admin"),
url(r"^faq/$", TemplateView.as_view(template_name="faq.html"),
name="faq"),
url(r"^faq/$", FAQView.as_view(), {'location':'faq', 'sublocation':'all'}, name="faq"),
url(r"^faq/(?P<location>\w*)/$", FAQView.as_view(), {'sublocation':'all'}),
url(r"^faq/(?P<location>\w*)/(?P<sublocation>\w*)/$", FAQView.as_view()),
url(r"^wishlist/$", "wishlist", name="wishlist"),
url(r"^campaigns/(?P<pk>\d+)/$",CampaignFormView.as_view(), name="campaign_by_id"),
url(r"^campaigns/(?P<facet>\w*)$", CampaignListView.as_view(), name='campaign_list'),
@ -46,7 +47,7 @@ urlpatterns = patterns(
url(r"^googlebooks/(?P<googlebooks_id>.+)/$", "googlebooks", name="googlebooks"),
#may want to deprecate the following
url(r"^setup/work/(?P<work_id>\d+)/$", "work", {'action':'setup_campaign'}, name="setup_campaign"),
url(r"^pledge/(?P<work_id>\d+)/$", PledgeView.as_view(), name="pledge"),
url(r"^pledge/(?P<work_id>\d+)/$", login_required(PledgeView.as_view()), name="pledge"),
url(r"^celery/clear/$","clear_celery_tasks", name="clear_celery_tasks"),
url(r"^subjects/$", "subjects", name="subjects"),
url(r"^librarything/$", LibraryThingView.as_view(), name="librarything"),

View File

@ -263,7 +263,7 @@ class PledgeView(FormView):
p = PaymentManager(embedded=self.embedded)
# we should force login at this point -- or if no account, account creation, login, and return to this spot
# PledgeView is wrapped in login_required -- so in theory, user should never be None -- but I'll keep this logic here for now.
if self.request.user.is_authenticated():
user = self.request.user
else:
@ -634,30 +634,20 @@ def search(request):
# flag search result as on wishlist as appropriate
if not request.user.is_anonymous():
# get a list of all the googlebooks_ids for works on the user's wishlist
wishlist = request.user.wishlist
editions = models.Edition.objects.filter(work__wishlists__in=[wishlist])
googlebooks_ids = [e['googlebooks_id'] for e in editions.values('googlebooks_id')]
ungluers = userlists.other_users(request.user, 5)
# if the results is on their wishlist flag it
for result in results:
if result['googlebooks_id'] in googlebooks_ids:
result['on_wishlist'] = True
else:
result['on_wishlist'] = False
else:
ungluers = userlists.other_users(None, 5)
# also urlencode some parameters we'll need to pass to workstub in the title links
# needs to be done outside the if condition
works=[]
for result in results:
result['urlimage'] = urllib.quote_plus(sub('^https?:\/\/','', result['cover_image_thumbnail']).encode("utf-8"), safe='')
result['urlauthor'] = urllib.quote_plus(result['author'].encode("utf-8"), safe='')
result['urltitle'] = urllib.quote_plus(result['title'].encode("utf-8"), safe='')
try:
edition = models.Edition.objects.get(googlebooks_id=result['googlebooks_id'])
works.append(edition.work)
except models.Edition.DoesNotExist:
works.append(result)
context = {
"q": q,
"results": results,
"results": works,
"ungluers": ungluers
}
return render(request, 'search.html', context)
@ -752,6 +742,12 @@ class CampaignFormView(FormView):
logger.info("CampaignFormView paypal: Error " + str(t.reference))
return HttpResponse(response)
class FAQView(TemplateView):
template_name = "faq.html"
def get_context_data(self, **kwargs):
location = self.kwargs["location"]
sublocation = self.kwargs["sublocation"]
return {'location': location, 'sublocation': sublocation}
class GoodreadsDisplayView(TemplateView):
template_name = "goodreads_display.html"

View File

@ -2,8 +2,9 @@ from regluit.core.models import Campaign, Wishlist
from regluit.payment.models import Transaction, Receiver, PaymentResponse
from django.contrib.auth.models import User
from regluit.payment.parameters import *
from regluit.payment.paypal import Pay, Execute, IPN, IPN_TYPE_PAYMENT, IPN_TYPE_PREAPPROVAL, IPN_TYPE_ADJUSTMENT, IPN_PAY_STATUS_ACTIVE
from regluit.payment.paypal import Pay, Execute, IPN, IPN_TYPE_PAYMENT, IPN_TYPE_PREAPPROVAL, IPN_TYPE_ADJUSTMENT, IPN_PAY_STATUS_ACTIVE, IPN_PAY_STATUS_INCOMPLETE
from regluit.payment.paypal import Preapproval, IPN_PAY_STATUS_COMPLETED, CancelPreapproval, PaymentDetails, PreapprovalDetails, IPN_SENDER_STATUS_COMPLETED, IPN_TXN_STATUS_COMPLETED
from regluit.payment.paypal import RefundPayment
import uuid
import traceback
from datetime import datetime
@ -157,11 +158,11 @@ class PaymentManager( object ):
t.save()
# Check the amount
if t.amount != D(p.amount):
if t.max_amount != D(p.amount):
#append_element(doc, tran, "amount_ours", str(t.amount))
#append_element(doc, tran, "amount_theirs", str(p.amount))
preapproval_status["amount"] = {'ours':t.amount, 'theirs':p.amount}
t.amount = p.amount
preapproval_status["amount"] = {'ours':t.max_amount, 'theirs':p.amount}
t.max_amount = p.amount
t.save()
# append only if there was a change in status
@ -234,6 +235,23 @@ class PaymentManager( object ):
# Reason code indicates more details of the adjustment type
t.reason = ipn.reason_code
# Update the receiver status codes
for item in ipn.transactions:
try:
r = Receiver.objects.get(transaction=t, email=item['receiver'])
logger.info(item)
# one of the IPN_SENDER_STATUS codes defined in paypal.py, If we are doing delayed chained
# payments, then there is no status or id for non-primary receivers. Leave their status alone
r.status = item['status_for_sender_txn']
r.save()
except:
# Log an exception if we have a receiver that is not found. This will be hit
# for delayed chained payments as there is no status or id for the non-primary receivers yet
traceback.print_exc()
t.save()
elif ipn.transaction_type == IPN_TYPE_PREAPPROVAL:
@ -257,26 +275,40 @@ class PaymentManager( object ):
except:
traceback.print_exc()
def run_query(self, transaction_list, summary, pledged, authorized):
def run_query(self, transaction_list, summary, pledged, authorized, incomplete, completed):
'''
Generic query handler for returning summary and transaction info, see query_user, query_list and query_campaign
'''
if pledged:
pledged_list = transaction_list.filter(type=PAYMENT_TYPE_INSTANT,
status="COMPLETED")
status=IPN_PAY_STATUS_COMPLETED)
else:
pledged_list = []
if authorized:
authorized_list = transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION,
status="ACTIVE")
status=IPN_PAY_STATUS_ACTIVE)
else:
authorized_list = []
if incomplete:
incomplete_list = transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION,
status=IPN_PAY_STATUS_INCOMPLETE)
else:
incomplete_list = []
if completed:
completed_list = transaction_list.filter(type=PAYMENT_TYPE_AUTHORIZATION,
status=IPN_PAY_STATUS_COMPLETED)
else:
completed_list = []
if summary:
pledged_amount = D('0.00')
authorized_amount = D('0.00')
incomplete_amount = D('0.00')
completed_amount = D('0.00')
for t in pledged_list:
for r in t.receiver_set.all():
@ -288,15 +320,19 @@ class PaymentManager( object ):
for t in authorized_list:
authorized_amount += t.amount
amount = pledged_amount + authorized_amount
for t in incomplete_list:
incomplete_amount += t.amount
for t in completed_list:
completed_amount += t.amount
amount = pledged_amount + authorized_amount + incomplete_amount + completed_amount
return amount
else:
return pledged_list | authorized_list
return pledged_list | authorized_list | incomplete_list | completed_list
def query_user(self, user, summary=False, pledged=True, authorized=True):
def query_user(self, user, summary=False, pledged=True, authorized=True, incomplete=True, completed=True):
'''
query_user
@ -305,15 +341,17 @@ class PaymentManager( object ):
summary: if true, return a float of the total, if false, return a list of transactions
pledged: include amounts pledged
authorized: include amounts pre-authorized
incomplete: include amounts for transactions with INCOMPLETE status
completed: include amounts for transactions that are COMPLETED
return value: either a float summary or a list of transactions
'''
transactions = Transaction.objects.filter(user=user)
return self.run_query(transactions, summary, pledged, authorized)
return self.run_query(transactions, summary, pledged, authorized, incomplete=True, completed=True)
def query_campaign(self, campaign, summary=False, pledged=True, authorized=True):
def query_campaign(self, campaign, summary=False, pledged=True, authorized=True, incomplete=True, completed=True):
'''
query_campaign
@ -322,16 +360,18 @@ class PaymentManager( object ):
summary: if true, return a float of the total, if false, return a list of transactions
pledged: include amounts pledged
authorized: include amounts pre-authorized
incomplete: include amounts for transactions with INCOMPLETE status
completed: includes payments that have been completed
return value: either a float summary or a list of transactions
'''
transactions = Transaction.objects.filter(campaign=campaign)
return self.run_query(transactions, summary, pledged, authorized)
return self.run_query(transactions, summary, pledged, authorized, incomplete, completed)
def query_list(self, list, summary=False, pledged=True, authorized=True):
def query_list(self, list, summary=False, pledged=True, authorized=True, incomplete=True, completed=True):
'''
query_list
@ -340,13 +380,15 @@ class PaymentManager( object ):
summary: if true, return a float of the total, if false, return a list of transactions
pledged: include amounts pledged
authorized: include amounts pre-authorized
incomplete: include amounts for transactions with INCOMPLETE status
completed: includes payments that have been completed
return value: either a float summary or a list of transactions
'''
transactions = Transaction.objects.filter(list=list)
return self.run_query(transactions, summary, pledged, authorized)
return self.run_query(transactions, summary, pledged, authorized, incomplete, completed)
def execute_campaign(self, campaign):
'''
@ -566,6 +608,7 @@ class PaymentManager( object ):
'''
t = Transaction.objects.create(amount=amount,
max_amount=amount,
type=PAYMENT_TYPE_AUTHORIZATION,
execution = EXECUTE_TYPE_CHAINED_DELAYED,
target=target,
@ -605,6 +648,121 @@ class PaymentManager( object ):
logger.info("Authorize Error: " + p.error_string())
return t, None
def modify_transaction(self, transaction, amount=None, expiry=None, return_url=None, cancel_url=None):
'''
modify
Modifies a transaction. The only type of modification allowed is to the amount and expiration date
amount: the new amount
expiry: the new expiration date, or if none the current expiration date will be used
return_url: the return URL after the preapproval(if needed)
cancel_url: the cancel url after the preapproval(if needed)
return value: True if successful, false otherwise. An optional second parameter for the forward URL if a new authorhization is needed
'''
if not amount:
logger.info("Error, no amount speicified")
return False
if transaction.type != PAYMENT_TYPE_AUTHORIZATION:
# Can only modify the amount of a preapproval for now
logger.info("Error, attempt to modify an invalid transaction type")
return False, None
if transaction.status != IPN_PAY_STATUS_ACTIVE:
# Can only modify an active, pending transaction. If it is completed, we need to do a refund. If it is incomplete,
# then an IPN may be pending and we cannot touch it
logger.info("Error, attempt to modify a transaction that is not active")
return False, None
if not expiry:
# Use the old expiration date
expiry = transaction.date_expired
if amount > transaction.max_amount or expiry != transaction.date_expired:
# Increase or expiuration change, cancel and start again
self.cancel_transaction(transaction)
# Start a new authorization for the new amount
t, url = self.authorize(transaction.currency,
transaction.target,
amount,
expiry,
transaction.campaign,
transaction.list,
transaction.user,
return_url,
cancel_url,
transaction.anonymous)
if t and url:
# Need to re-direct to approve the transaction
logger.info("New authorization needed, redirectiont to url %s" % url)
return True, url
else:
# No amount change necessary
logger.info("Error, unable to start a new authorization")
return False, None
elif amount <= transaction.max_amount:
# Change the amount but leave the preapproval alone
transaction.amount = amount
transaction.save()
logger.info("Updated amount of transaction to %f" % amount)
return True, None
else:
# No changes
logger.info("Error, no modifications requested")
return False, None
def refund_transaction(self, transaction):
'''
refund
Refunds a transaction. The money for the transaction may have gone to a number of places. We can only
refund money that is in our account
return value: True if successful, false otherwise
'''
# First check if a payment has been made. It is possible that some of the receivers may be incomplete
# We need to verify that the refund API will cancel these
if transaction.status != IPN_PAY_STATUS_COMPLETED:
logger.info("Refund Transaction failed, invalid transaction status")
return False
p = RefundPayment(transaction)
# Create a response for this
envelope = p.envelope()
if envelope:
correlation = p.correlation_id()
timestamp = p.timestamp()
r = PaymentResponse.objects.create(api=p.url,
correlation_id = correlation,
timestamp = timestamp,
info = p.raw_response,
transaction=transaction)
if p.success() and not p.error():
logger.info("Refund Transaction " + str(transaction.id) + " Completed")
return True
else:
transaction.error = p.error_string()
transaction.save()
logger.info("Refund Transaction " + str(transaction.id) + " Failed with error: " + p.error_string())
return False
def pledge(self, currency, target, receiver_list, campaign=None, list=None, user=None, return_url=None, cancel_url=None, anonymous=False):
'''
pledge
@ -635,6 +793,7 @@ class PaymentManager( object ):
amount = D(receiver_list[0]['amount'])
t = Transaction.objects.create(amount=amount,
max_amount=amount,
type=PAYMENT_TYPE_INSTANT,
execution=EXECUTE_TYPE_CHAINED_INSTANT,
target=target,

View File

@ -0,0 +1,150 @@
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Transaction.max_amount'
db.add_column('payment_transaction', 'max_amount', self.gf('django.db.models.fields.DecimalField')(default='0.00', max_digits=14, decimal_places=2), keep_default=False)
def backwards(self, orm):
# Deleting field 'Transaction.max_amount'
db.delete_column('payment_transaction', 'max_amount')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'core.campaign': {
'Meta': {'object_name': 'Campaign'},
'activated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'amazon_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'deadline': ('django.db.models.fields.DateTimeField', [], {}),
'description': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'details': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'left': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}),
'managers': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'campaigns'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '500', 'null': 'True'}),
'paypal_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'INITIALIZED'", 'max_length': '15', 'null': 'True'}),
'target': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '14', 'decimal_places': '2'}),
'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'to': "orm['core.Work']"})
},
'core.wishes': {
'Meta': {'object_name': 'Wishes', 'db_table': "'core_wishlist_works'"},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'source': ('django.db.models.fields.CharField', [], {'max_length': '15', 'blank': 'True'}),
'wishlist': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Wishlist']"}),
'work': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Work']"})
},
'core.wishlist': {
'Meta': {'object_name': 'Wishlist'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'wishlist'", 'unique': 'True', 'to': "orm['auth.User']"}),
'works': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'wishlists'", 'symmetrical': 'False', 'through': "orm['core.Wishes']", 'to': "orm['core.Work']"})
},
'core.work': {
'Meta': {'ordering': "['title']", 'object_name': 'Work'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '2'}),
'librarything_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True'}),
'openlibrary_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True'}),
'openlibrary_lookup': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'})
},
'payment.paymentresponse': {
'Meta': {'object_name': 'PaymentResponse'},
'api': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'correlation_id': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'info': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}),
'timestamp': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
'transaction': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['payment.Transaction']"})
},
'payment.receiver': {
'Meta': {'object_name': 'Receiver'},
'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}),
'currency': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'email': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'primary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'reason': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'status': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'transaction': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['payment.Transaction']"}),
'txn_id': ('django.db.models.fields.CharField', [], {'max_length': '64'})
},
'payment.transaction': {
'Meta': {'object_name': 'Transaction'},
'amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}),
'anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'campaign': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Campaign']", 'null': 'True'}),
'currency': ('django.db.models.fields.CharField', [], {'default': "'USD'", 'max_length': '10', 'null': 'True'}),
'date_authorized': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'date_executed': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'date_expired': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'date_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'date_payment': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'error': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True'}),
'execution': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'list': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['core.Wishlist']", 'null': 'True'}),
'max_amount': ('django.db.models.fields.DecimalField', [], {'default': "'0.00'", 'max_digits': '14', 'decimal_places': '2'}),
'pay_key': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
'preapproval_key': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
'reason': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
'receipt': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True'}),
'secret': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'NONE'", 'max_length': '32'}),
'target': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'type': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'})
}
}
complete_apps = ['payment']

View File

@ -21,6 +21,7 @@ class Transaction(models.Model):
# amount & currency -- amount of money and its currency involved for transaction
amount = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99
max_amount = models.DecimalField(default=Decimal('0.00'), max_digits=14, decimal_places=2) # max 999,999,999,999.99
currency = models.CharField(max_length=10, default='USD', null=True)
# a unique ID that can be passed to PayPal to track a transaction
@ -96,6 +97,9 @@ class PaymentResponse(models.Model):
transaction = models.ForeignKey(Transaction, null=False)
def __unicode__(self):
return u"PaymentResponse -- api: {0} correlation_id: {1} transaction: {2}".format(self.api, self.correlation_id, unicode(self.transaction))
class Receiver(models.Model):
@ -110,6 +114,9 @@ class Receiver(models.Model):
txn_id = models.CharField(max_length=64)
transaction = models.ForeignKey(Transaction)
def __unicode__(self):
return u"Receiver -- email: {0} status: {1} transaction: {2}".format(self.email, self.status, unicode(self.transaction))
from django.db.models.signals import post_save, post_delete
import regluit.payment.manager

View File

@ -7,11 +7,11 @@ EXECUTE_TYPE_CHAINED_INSTANT = 1
EXECUTE_TYPE_CHAINED_DELAYED = 2
EXECUTE_TYPE_PARALLEL = 3
TARGET_TYPE_NONE = 0
TARGET_TYPE_CAMPAIGN = 1
TARGET_TYPE_LIST = 2
TARGET_TYPE_DONATION = 3
# these two following parameters are probably extraneous since I think we will compute dynamically where to return each time.
COMPLETE_URL = '/paymentcomplete'
CANCEL_URL = '/paymentcancel'

View File

@ -35,11 +35,20 @@ IPN_TYPE_ADJUSTMENT = 'Adjustment'
IPN_TYPE_PREAPPROVAL = 'Adaptive Payment PREAPPROVAL'
#pay API status constants
# I think 'NONE' is not something the API produces but is particular to our implementation
# couldn't we use the Python None?
# NONE' is not something the API produces but is particular to our implementation
IPN_PAY_STATUS_NONE = 'NONE'
# The following apply to INSTANT PAYMENTS
#The following apply to INSTANT PAYMENTS
#https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_APPayAPI
#CREATED - The payment request was received; funds will be transferred once the payment is approved
#COMPLETED - The payment was successful
#INCOMPLETE - Some transfers succeeded and some failed for a parallel payment or, for a delayed chained payment, secondary receivers have not been paid
#ERROR - The payment failed and all attempted transfers failed or all completed transfers were successfully reversed
#REVERSALERROR - One or more transfers failed when attempting to reverse a payment
#PROCESSING - The payment is in progress
#PENDING - The payment is awaiting processing
IPN_PAY_STATUS_CREATED = 'CREATED'
IPN_PAY_STATUS_COMPLETED = 'COMPLETED'
IPN_PAY_STATUS_INCOMPLETE = 'INCOMPLETE'
@ -49,10 +58,26 @@ IPN_PAY_STATUS_PROCESSING = 'PROCESSING'
IPN_PAY_STATUS_PENDING = 'PENDING'
# particular to preapprovals -- may want to rename these constants to IPN_PREAPPROVAL_STATUS_*
# https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_APPreapprovalDetails
#ACTIVE - The preapproval is active
#CANCELED - The preapproval was explicitly canceled by the sender or by PayPal
#DEACTIVED - The preapproval is not active; you can be reactivate it by resetting the personal identification number (PIN) or by contacting PayPal
IPN_PAY_STATUS_ACTIVE = "ACTIVE"
IPN_PAY_STATUS_CANCELED = "CANCELED"
IPN_PAY_STATUS_DEACTIVED = "DEACTIVED"
# https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_APIPN
#COMPLETED - The sender's transaction has completed
#PENDING - The transaction is awaiting further processing
#CREATED - The payment request was received; funds will be transferred once approval is received
#PARTIALLY_REFUNDED - Transaction was partially refunded
#DENIED - The transaction was rejected by the receiver
#PROCESSING - The transaction is in progress
#REVERSED - The payment was returned to the sender
#REFUNDED - The payment was refunded
#FAILED - The payment failed
IPN_SENDER_STATUS_COMPLETED = 'COMPLETED'
IPN_SENDER_STATUS_PENDING = 'PENDING'
IPN_SENDER_STATUS_CREATED = 'CREATED'
@ -67,7 +92,7 @@ IPN_SENDER_STATUS_FAILED = 'FAILED'
IPN_ACTION_TYPE_PAY = 'PAY'
IPN_ACTION_TYPE_CREATE = 'CREATE'
# individual sender transaction constants
# individual sender transaction constants (???)
IPN_TXN_STATUS_COMPLETED = 'Completed'
IPN_TXN_STATUS_PENDING = 'Pending'
IPN_TXN_STATUS_REFUNDED = 'Refunded'
@ -130,7 +155,8 @@ class PaypalEnvelopeRequest:
url = None
def ack( self ):
if self.response.has_key( 'responseEnvelope' ) and self.response['responseEnvelope'].has_key( 'ack' ):
if self.response and self.response.has_key( 'responseEnvelope' ) and self.response['responseEnvelope'].has_key( 'ack' ):
return self.response['responseEnvelope']['ack']
else:
return None
@ -152,17 +178,22 @@ class PaypalEnvelopeRequest:
return False
def error_data(self):
if self.response.has_key('error'):
if self.response and self.response.has_key('error'):
return self.response['error']
else:
return None
def error_id(self):
if self.response.has_key('error'):
if self.response and self.response.has_key('error'):
return self.response['error'][0]['errorId']
else:
return None
def error_string(self):
if self.response.has_key('error'):
if self.response and self.response.has_key('error'):
return self.response['error'][0]['message']
elif self.errorMessage:
@ -172,19 +203,20 @@ class PaypalEnvelopeRequest:
return None
def envelope(self):
if self.response.has_key('responseEnvelope'):
if self.response and self.response.has_key('responseEnvelope'):
return self.response['responseEnvelope']
else:
return None
def correlation_id(self):
if self.response.has_key('responseEnvelope') and self.response['responseEnvelope'].has_key('correlationId'):
if self.response and self.response.has_key('responseEnvelope') and self.response['responseEnvelope'].has_key('correlationId'):
return self.response['responseEnvelope']['correlationId']
else:
return None
def timestamp(self):
if self.response.has_key('responseEnvelope') and self.response['responseEnvelope'].has_key('timestamp'):
if self.response and self.response.has_key('responseEnvelope') and self.response['responseEnvelope'].has_key('timestamp'):
return self.response['responseEnvelope']['timestamp']
else:
return None
@ -509,6 +541,46 @@ class CancelPreapproval(PaypalEnvelopeRequest):
self.errorMessage = "Error: Server Error"
class RefundPayment(PaypalEnvelopeRequest):
def __init__(self, transaction):
try:
headers = {
'X-PAYPAL-SECURITY-USERID':settings.PAYPAL_USERNAME,
'X-PAYPAL-SECURITY-PASSWORD':settings.PAYPAL_PASSWORD,
'X-PAYPAL-SECURITY-SIGNATURE':settings.PAYPAL_SIGNATURE,
'X-PAYPAL-APPLICATION-ID':settings.PAYPAL_APPID,
'X-PAYPAL-REQUEST-DATA-FORMAT':'JSON',
'X-PAYPAL-RESPONSE-DATA-FORMAT':'JSON',
}
data = {
'payKey':transaction.pay_key,
'requestEnvelope': { 'errorLanguage': 'en_US' }
}
self.raw_request = json.dumps(data)
self.headers = headers
self.url = "/AdaptivePayments/Refund"
self.connection = url_request(self)
self.code = self.connection.code()
if self.code != 200:
self.errorMessage = 'PayPal response code was %i' % self.code
return
self.raw_response = self.connection.content()
logger.info("paypal Refund response was: %s" % self.raw_response)
self.response = json.loads( self.raw_response )
logger.info(self.response)
except:
traceback.print_exc()
self.errorMessage = "Error: Server Error"
class Preapproval( PaypalEnvelopeRequest ):
def __init__( self, transaction, amount, expiry=None, return_url=None, cancel_url=None):

View File

@ -7,8 +7,9 @@ Replace this with more appropriate tests for your application.
from django.test import TestCase
from django.utils import unittest
from django.conf import settings
from regluit.payment.manager import PaymentManager
from regluit.payment.paypal import IPN, IPN_PAY_STATUS_ACTIVE, IPN_PAY_STATUS_COMPLETED, IPN_TXN_STATUS_COMPLETED
from regluit.payment.paypal import IPN, IPN_PAY_STATUS_ACTIVE, IPN_PAY_STATUS_COMPLETED, IPN_TXN_STATUS_COMPLETED, IPN_PAY_STATUS_COMPLETED, IPN_TXN_STATUS_COMPLETED
from noseselenium.cases import SeleniumTestCaseMixin
from regluit.payment.models import Transaction
from regluit.core.models import Campaign, Wishlist, Work
@ -31,8 +32,8 @@ def loginSandbox(test, selenium):
selenium.open('https://developer.paypal.com/')
time.sleep(5)
test.failUnless(selenium.is_text_present('Member Log In'))
selenium.type('login_email', PAYPAL_SANDBOX_LOGIN)
selenium.type('login_password', PAYPAL_SANDBOX_PASSWORD)
selenium.type('login_email', settings.PAYPAL_SANDBOX_LOGIN)
selenium.type('login_password', settings.PAYPAL_SANDBOX_PASSWORD)
time.sleep(2)
selenium.click('css=input[class=\"formBtnOrange\"]')
time.sleep(5)
@ -50,8 +51,8 @@ def authorizeSandbox(test, selenium, url):
test.failUnless(selenium.is_text_present('Your preapproved payment summary'))
selenium.click('loadLogin')
time.sleep(5)
selenium.type('id=login_email', PAYPAL_BUYER_LOGIN)
selenium.type('id=login_password', PAYPAL_BUYER_PASSWORD)
selenium.type('id=login_email', settings.PAYPAL_BUYER_LOGIN)
selenium.type('id=login_password', settings.PAYPAL_BUYER_PASSWORD)
time.sleep(2)
selenium.click('submitLogin')
time.sleep(5)
@ -63,6 +64,7 @@ def authorizeSandbox(test, selenium, url):
except:
traceback.print_exc()
def paySandbox(test, selenium, url):
print "PAY SANDBOX"
@ -73,8 +75,8 @@ def paySandbox(test, selenium, url):
test.failUnless(selenium.is_text_present('Your payment summary'))
selenium.click('loadLogin')
time.sleep(5)
selenium.type('id=login_email', PAYPAL_BUYER_LOGIN)
selenium.type('id=login_password', PAYPAL_BUYER_PASSWORD)
selenium.type('id=login_email', settings.PAYPAL_BUYER_LOGIN)
selenium.type('id=login_password', settings.PAYPAL_BUYER_PASSWORD)
time.sleep(2)
selenium.click('submitLogin')
time.sleep(5)
@ -98,7 +100,6 @@ class PledgeTest(TestCase):
"http://www.google.com/")
self.selenium.start()
def validateRedirect(self, t, url, count):
self.assertNotEqual(url, None)
@ -106,9 +107,9 @@ class PledgeTest(TestCase):
self.assertEqual(t.receiver_set.all().count(), count)
self.assertEqual(t.receiver_set.all()[0].amount, t.amount)
self.assertEqual(t.receiver_set.all()[0].currency, t.currency)
self.assertNotEqual(t.reference, None)
# self.assertNotEqual(t.reference, None)
self.assertEqual(t.error, None)
self.assertEqual(t.status, 'NONE')
self.assertEqual(t.status, IPN_PAY_STATUS_CREATED)
valid = URLValidator(verify_exists=True)
try:
@ -116,14 +117,14 @@ class PledgeTest(TestCase):
except ValidationError, e:
print e
@unittest.expectedFailure
def test_pledge_single_receiver(self):
try:
p = PaymentManager()
# Note, set this to 1-5 different receivers with absolute amounts for each
receiver_list = [{'email':'jakace_1309677337_biz@gmail.com', 'amount':20.00}]
receiver_list = [{'email':settings.PAYPAL_GLUEJAR_EMAIL, 'amount':20.00}]
t, url = p.pledge('USD', TARGET_TYPE_NONE, receiver_list, campaign=None, list=None, user=None)
self.validateRedirect(t, url, 1)
@ -131,22 +132,25 @@ class PledgeTest(TestCase):
loginSandbox(self, self.selenium)
paySandbox(self, self.selenium, url)
# by now we should have received the IPN
# right now, for running on machine with no acess to IPN, we manually update statuses
p.checkStatus()
t = Transaction.objects.get(id=t.id)
# by now we should have received the IPN
self.assertEqual(t.status, IPN_PAY_STATUS_COMPLETED)
self.assertEqual(t.receiver_set.all()[0].status, IPN_TXN_STATUS_COMPLETED)
except:
traceback.print_exc()
@unittest.expectedFailure
def test_pledge_mutiple_receiver(self):
p = PaymentManager()
# Note, set this to 1-5 different receivers with absolute amounts for each
receiver_list = [{'email':'jakace_1309677337_biz@gmail.com', 'amount':20.00},
{'email':'seller_1317463643_biz@gmail.com', 'amount':10.00}]
receiver_list = [{'email':settings.PAYPAL_GLUEJAR_EMAIL, 'amount':20.00},
{'email':settings.PAYPAL_TEST_RH_EMAIL, 'amount':10.00}]
t, url = p.pledge('USD', TARGET_TYPE_NONE, receiver_list, campaign=None, list=None, user=None)
@ -155,23 +159,30 @@ class PledgeTest(TestCase):
loginSandbox(self, self.selenium)
paySandbox(self, self.selenium, url)
# by now we should have received the IPN
# right now, for running on machine with no acess to IPN, we manually update statuses
p.checkStatus()
t = Transaction.objects.get(id=t.id)
# by now we should have received the IPN
self.assertEqual(t.status, IPN_PAY_STATUS_COMPLETED)
self.assertEqual(t.receiver_set.all()[0].status, IPN_TXN_STATUS_COMPLETED)
self.assertEqual(t.receiver_set.all()[1].status, IPN_TXN_STATUS_COMPLETED)
@unittest.expectedFailure
def test_pledge_too_much(self):
p = PaymentManager()
# Note, set this to 1-5 different receivers with absolute amounts for each
receiver_list = [{'email':'jakace_1309677337_biz@gmail.com', 'amount':50000.00}]
receiver_list = [{'email':settings.PAYPAL_GLUEJAR_EMAIL, 'amount':50000.00}]
t, url = p.pledge('USD', TARGET_TYPE_NONE, receiver_list, campaign=None, list=None, user=None)
self.validateRedirect(t, url, 1)
def tearDown(self):
self.selenium.stop()
class AuthorizeTest(TestCase):
def setUp(self):
@ -187,7 +198,7 @@ class AuthorizeTest(TestCase):
self.assertNotEqual(url, None)
self.assertNotEqual(t, None)
self.assertNotEqual(t.reference, None)
#self.assertNotEqual(t.reference, None)
self.assertEqual(t.error, None)
self.assertEqual(t.status, 'NONE')
@ -212,10 +223,16 @@ class AuthorizeTest(TestCase):
loginSandbox(self, self.selenium)
authorizeSandbox(self, self.selenium, url)
# stick in a getStatus to update statuses in the absence of IPNs
p.checkStatus()
t = Transaction.objects.get(id=t.id)
self.assertEqual(t.status, IPN_PAY_STATUS_ACTIVE)
def tearDown(self):
self.selenium.stop()
class TransactionTest(TestCase):
def setUp(self):
"""
@ -246,10 +263,9 @@ class TransactionTest(TestCase):
def suite():
#testcases = [PledgeTest, AuthorizeTest]
#testcases = [PledgeTest, AuthorizeTest, TransactionTest]
testcases = [TransactionTest]
suites = unittest.TestSuite([unittest.TestLoader().loadTestsFromTestCase(testcase) for testcase in testcases])
return suites

View File

@ -12,4 +12,6 @@ urlpatterns = patterns(
url(r"^paymentcomplete","paymentcomplete"),
url(r"^checkstatus", "checkStatus"),
url(r"^testfinish", "testFinish"),
url(r"^testrefund", "testRefund"),
url(r"^testmodify", "testModify"),
)

View File

@ -16,7 +16,7 @@ import logging
logger = logging.getLogger(__name__)
# parameterize some test recipients
TEST_RECEIVERS = ['jakace_1309677337_biz@gmail.com', 'seller_1317463643_biz@gmail.com']
TEST_RECEIVERS = ['seller_1317463643_biz@gmail.com', 'Buyer6_1325742408_per@gmail.com']
#TEST_RECEIVERS = ['glueja_1317336101_biz@gluejar.com', 'rh1_1317336251_biz@gluejar.com', 'RH2_1317336302_biz@gluejar.com']
@ -114,8 +114,8 @@ def testAuthorize(request):
return HttpResponseRedirect(url)
else:
response = t.reference
logger.info("testAuthorize: Error " + str(t.reference))
response = t.error
logger.info("testAuthorize: Error " + str(t.error))
return HttpResponse(response)
'''
@ -136,6 +136,58 @@ def testCancel(request):
message = "Error: " + t.error
return HttpResponse(message)
'''
http://BASE/testrefund?transaction=2
Example that refunds a transaction
'''
def testRefund(request):
if "transaction" not in request.GET.keys():
return HttpResponse("No Transaction in Request")
t = Transaction.objects.get(id=int(request.GET['transaction']))
p = PaymentManager()
if p.refund_transaction(t):
return HttpResponse("Success")
else:
if t.error:
message = "Error: " + t.error
else:
message = "Error"
return HttpResponse(message)
'''
http://BASE/testmodify?transaction=2
Example that modifies the amount of a transaction
'''
def testModify(request):
if "transaction" not in request.GET.keys():
return HttpResponse("No Transaction in Request")
if "amount" in request.GET.keys():
amount = float(request.GET['amount'])
else:
amount = 200.0
t = Transaction.objects.get(id=int(request.GET['transaction']))
p = PaymentManager()
status, url = p.modify_transaction(t, amount)
if url:
logger.info("testModify: " + url)
return HttpResponseRedirect(url)
if status:
return HttpResponse("Success")
else:
return HttpResponse("Error")
'''
http://BASE/testfinish?transaction=2

View File

@ -233,7 +233,7 @@ ul.navigation li a:hover, ul.navigation li.active a {
.listview.icons .booklist-status-img img {
padding: 5px;
}
.listview.icons .booklist-status-label, .listview.icons .right_add {
.listview.icons .booklist-status-label {
display: none;
}
div.content-block-content {

View File

@ -119,14 +119,12 @@
left: 40px;
bottom: 5px;
}
.panelview.icons .right_add {
float: right;
padding: 10px 10px 0 0;
width: 24px;
}
.panelview.icons .panelnope {
display: none;
}
.panelview.icons .rounded {
margin-bottom: 7px;
}
.panelview.boolist-ebook a {
display: none;
}
@ -365,7 +363,7 @@ div.panelview.side2 {
}
/* title, author */
.white_text {
width: 130px;
width: 120px;
height: 45px;
padding: 15px 0px;
margin: 0px;
@ -443,16 +441,6 @@ div.panelview.side2 {
margin: 0px;
float: left;
}
.right_add {
padding: 10px;
margin: 0px;
float: right;
}
p.right_add {
float: right;
padding: 10px 10px 0 0;
width: 24px;
}
/**/
.read2 {
margin: 15px auto;

View File

@ -261,6 +261,9 @@ div.content-block-content {
padding: 5px;
border: solid 5px #EDF3F4;
}
.tabs-content form {
margin-left: -5px;
}
ul.social li {
padding: 5px 0 5px 30px;
height: 28px;
@ -348,20 +351,19 @@ ul.support li:hover {
ul.support li:hover a {
color: #fff;
}
.work_supporter {
.work_supporter_nocomment {
height: 50px;
margin-top: 5px;
vertical-align: middle;
margin-left: -5px;
}
.work_supporter .work_supporter_avatar {
.work_supporter_avatar {
float: left;
}
.work_supporter .work_supporter_name {
.work_supporter_name {
height: 50px;
line-height: 50px;
float: left;
margin-left: 5px;
}
/* this line differs from sitewide.css. should it? */
a {
@ -417,3 +419,16 @@ a {
.editions a:hover {
text-decoration: underline;
}
.thank-you {
font-size: 20px;
}
.work_supporter {
height: auto;
min-height: 50px;
margin-top: 5px;
vertical-align: middle;
margin-left: -5px;
}
.work_supporter_avatar {
margin-right: 5px;
}

View File

@ -13,14 +13,64 @@
border-style: solid none;
border-color: #FFFFFF;
}
.user-block {
width: 100%;
clear: both;
/* variables and mixins used in multiple less files go here */
.header-text {
height: 36px;
line-height: 36px;
display: block;
text-decoration: none;
font-weight: bold;
font-size: 13px;
letter-spacing: -0.05em;
}
.panelborders {
border-width: 1px 0px;
border-style: solid none;
border-color: #FFFFFF;
}
.user-block-hide {
float: left;
width: 100%;
clear: both;
border-top: solid 1px #8ac3d7;
margin-top: 20px;
}
.user-block-hide .quicktour {
width: 270px;
float: left;
font-style: italic;
line-height: 20px;
font-size: 13px;
margin-top: 20px;
}
.user-block-hide .quicktour .highlight {
font-weight: bold;
}
.user-block-hide .quicktour.last {
padding-right: 0px;
width: 270px;
background: url("/static/images/landingpage/signmeup-arrow.png") no-repeat center bottom;
padding-bottom: 42px;
}
.user-block-hide .movingrightalong {
background: url("/static/images/landingpage/quicktour-arrow.png") no-repeat center;
height: 100px;
width: 75px;
float: left;
margin-top: 20px;
}
.block-intro-text div {
display: none;
line-height: 25px;
}
.block-intro-text div#active {
display: inherit;
height: 75px;
}
/* Learn More area (not already styles by learnmore.less) */
.user-block {
width: 100%;
clear: both;
}
.user-block1, .user-block2 {
float: left;
@ -55,6 +105,9 @@
color: #3d4e53;
padding-right: 15px;
}
.user-block-hide .quicktour.last {
background: none;
}
/* Containers */
.have-right #js-main-container {
float: left;
@ -69,6 +122,9 @@
-moz-border-radius: 12px 12px 12px 12px;
-webkit-border-radius: 12px 12px 12px 12px;
border-radius: 12px 12px 12px 12px;
-moz-border-radius: 12px 12px 12px 12px;
-webkit-border-radius: 12px 12px 12px 12px;
border-radius: 12px 12px 12px 12px;
}
.have-right #js-rightcol .jsmodule {
border-bottom: 1px solid #3c4e52;
@ -102,6 +158,9 @@
-moz-border-radius: 14px 14px 14px 14px;
-webkit-border-radius: 14px 14px 14px 14px;
border-radius: 14px 14px 14px 14px;
-moz-border-radius: 14px 14px 14px 14px;
-webkit-border-radius: 14px 14px 14px 14px;
border-radius: 14px 14px 14px 14px;
padding: 10px;
background: #edf3f4;
}
@ -110,6 +169,9 @@
-moz-border-radius: 12px 12px 12px 12px;
-webkit-border-radius: 12px 12px 12px 12px;
border-radius: 12px 12px 12px 12px;
-moz-border-radius: 12px 12px 12px 12px;
-webkit-border-radius: 12px 12px 12px 12px;
border-radius: 12px 12px 12px 12px;
padding: 10px;
font-style: italic;
}
@ -118,6 +180,9 @@ dt {
-moz-border-radius: 14px 14px 14px 14px;
-webkit-border-radius: 14px 14px 14px 14px;
border-radius: 14px 14px 14px 14px;
-moz-border-radius: 14px 14px 14px 14px;
-webkit-border-radius: 14px 14px 14px 14px;
border-radius: 14px 14px 14px 14px;
padding: 10px 0px 10px 7px;
background: #edf3f4;
}
@ -130,3 +195,6 @@ dd {
.errorlist li {
border: solid #8dc63f 4px;
}
.collapse ul {
display: none;
}

View File

@ -13,6 +13,60 @@
border-style: solid none;
border-color: #FFFFFF;
}
/* variables and mixins used in multiple less files go here */
.header-text {
height: 36px;
line-height: 36px;
display: block;
text-decoration: none;
font-weight: bold;
font-size: 13px;
letter-spacing: -0.05em;
}
.panelborders {
border-width: 1px 0px;
border-style: solid none;
border-color: #FFFFFF;
}
.user-block-hide {
float: left;
width: 100%;
clear: both;
border-top: solid 1px #8ac3d7;
margin-top: 20px;
}
.user-block-hide .quicktour {
width: 270px;
float: left;
font-style: italic;
line-height: 20px;
font-size: 13px;
margin-top: 20px;
}
.user-block-hide .quicktour .highlight {
font-weight: bold;
}
.user-block-hide .quicktour.last {
padding-right: 0px;
width: 270px;
background: url("/static/images/landingpage/signmeup-arrow.png") no-repeat center bottom;
padding-bottom: 42px;
}
.user-block-hide .movingrightalong {
background: url("/static/images/landingpage/quicktour-arrow.png") no-repeat center;
height: 100px;
width: 75px;
float: left;
margin-top: 20px;
}
.block-intro-text div {
display: none;
line-height: 25px;
}
.block-intro-text div#active {
display: inherit;
height: 75px;
}
#expandable {
display: none;
}
@ -45,37 +99,6 @@
width: 100%;
clear: both;
}
.user-block-hide {
float: left;
width: 100%;
clear: both;
border-top: solid 1px #8ac3d7;
margin-top: 20px;
}
.user-block-hide .quicktour {
width: 270px;
float: left;
font-style: italic;
line-height: 20px;
font-size: 13px;
margin-top: 20px;
}
.user-block-hide .quicktour .highlight {
font-weight: bold;
}
.user-block-hide .quicktour.last {
padding-right: 0px;
width: 270px;
background: url("/static/images/landingpage/signmeup-arrow.png") no-repeat center bottom;
padding-bottom: 42px;
}
.user-block-hide .movingrightalong {
background: url("/static/images/landingpage/quicktour-arrow.png") no-repeat center;
height: 100px;
width: 75px;
float: left;
margin-top: 20px;
}
.user-block1, .user-block2 {
float: left;
}
@ -106,6 +129,8 @@
font-size: 20px;
height: 30px;
line-height: 30px;
height: 30px;
line-height: 30px;
color: #3d4e53;
padding-right: 15px;
}
@ -113,10 +138,15 @@
float: right;
height: 24px;
line-height: 24px;
height: 24px;
line-height: 24px;
width: 24px;
-moz-border-radius: 12px 12px 12px 12px;
-webkit-border-radius: 12px 12px 12px 12px;
border-radius: 12px 12px 12px 12px;
-moz-border-radius: 12px 12px 12px 12px;
-webkit-border-radius: 12px 12px 12px 12px;
border-radius: 12px 12px 12px 12px;
border: solid 4px #3d4e53;
text-align: center;
font-size: 17px;
@ -136,6 +166,9 @@
-moz-border-radius: 12px 12px 12px 12px;
-webkit-border-radius: 12px 12px 12px 12px;
border-radius: 12px 12px 12px 12px;
-moz-border-radius: 12px 12px 12px 12px;
-webkit-border-radius: 12px 12px 12px 12px;
border-radius: 12px 12px 12px 12px;
margin-bottom: 10px;
padding: 0 10px 10px 10px;
}
@ -144,12 +177,17 @@
padding-bottom: 10px;
}
#js-rightcol .jsmodule input, #js-rightcol2 .jsmodule input {
-moz-border-radius: 32px 32px 32px 32px;
-webkit-border-radius: 32px 32px 32px 32px;
border-radius: 32px 32px 32px 32px;
-moz-border-radius: 32px 32px 32px 32px;
-webkit-border-radius: 32px 32px 32px 32px;
border-radius: 32px 32px 32px 32px;
border: none;
height: 36px;
line-height: 36px;
height: 36px;
line-height: 36px;
width: 90%;
outline: none;
padding-left: 16px;
@ -227,6 +265,9 @@ div.typo2 {
-moz-border-radius: 12px 12px 12px 12px;
-webkit-border-radius: 12px 12px 12px 12px;
border-radius: 12px 12px 12px 12px;
-moz-border-radius: 12px 12px 12px 12px;
-webkit-border-radius: 12px 12px 12px 12px;
border-radius: 12px 12px 12px 12px;
padding: 10px;
font-style: italic;
}
@ -242,6 +283,8 @@ div.signup_btn a {
font-weight: bold;
height: 36px;
line-height: 36px;
height: 36px;
line-height: 36px;
letter-spacing: 1px;
text-decoration: none;
text-transform: capitalize;
@ -295,6 +338,9 @@ h2.page-heading {
margin-top: 55px;
}
#js-maincontainer-bot-block #js-search {
-moz-border-radius: 64px 64px 64px 64px;
-webkit-border-radius: 64px 64px 64px 64px;
border-radius: 64px 64px 64px 64px;
-moz-border-radius: 64px 64px 64px 64px;
-webkit-border-radius: 64px 64px 64px 64px;
border-radius: 64px 64px 64px 64px;
@ -327,6 +373,8 @@ h2.page-heading {
color: #66942e;
height: 26px;
line-height: 26px;
height: 26px;
line-height: 26px;
font-size: 13px;
float: left;
padding: 0;
@ -375,6 +423,9 @@ h2.page-heading {
-moz-border-radius: 10px 10px 0 0;
-webkit-border-radius: 10px 10px 0 0;
border-radius: 10px 10px 0 0;
-moz-border-radius: 10px 10px 0 0;
-webkit-border-radius: 10px 10px 0 0;
border-radius: 10px 10px 0 0;
font-size: 18px;
overflow: hidden;
display: inline-block;
@ -389,6 +440,8 @@ h3.module-title {
padding: 14px 0;
}
.google_signup div {
height: 32px;
line-height: 32px;
height: 32px;
line-height: 32px;
float: left;

54
static/css/learnmore.css Normal file
View File

@ -0,0 +1,54 @@
/* variables and mixins used in multiple less files go here */
.header-text {
height: 36px;
line-height: 36px;
display: block;
text-decoration: none;
font-weight: bold;
font-size: 13px;
letter-spacing: -0.05em;
}
.panelborders {
border-width: 1px 0px;
border-style: solid none;
border-color: #FFFFFF;
}
.user-block-hide {
float: left;
width: 100%;
clear: both;
border-top: solid 1px #8ac3d7;
margin-top: 20px;
}
.user-block-hide .quicktour {
width: 270px;
float: left;
font-style: italic;
line-height: 20px;
font-size: 13px;
margin-top: 20px;
}
.user-block-hide .quicktour .highlight {
font-weight: bold;
}
.user-block-hide .quicktour.last {
padding-right: 0px;
width: 270px;
background: url("/static/images/landingpage/signmeup-arrow.png") no-repeat center bottom;
padding-bottom: 42px;
}
.user-block-hide .movingrightalong {
background: url("/static/images/landingpage/quicktour-arrow.png") no-repeat center;
height: 100px;
width: 75px;
float: left;
margin-top: 20px;
}
.block-intro-text div {
display: none;
line-height: 25px;
}
.block-intro-text div#active {
display: inherit;
height: 75px;
}

View File

@ -88,6 +88,13 @@ ul.menu {
padding: 0;
margin: 0;
}
/* Learn More menu */
.block-intro-text {
padding-right: 10px;
}
.block-intro-text span.def {
font-style: italic;
}
a.readon {
background: url("/static/images/learnmore-downarrow.png") right center no-repeat;
color: #fff;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

30
static/js/definitions.js Normal file
View File

@ -0,0 +1,30 @@
// expand or collapse the learn more section
var $j = jQuery.noConflict();
$j(document).ready(function(){
$j('.user-block-hide').hide();
$j('.user-block1 a').click(
function() {
$j(this).toggleClass("active");
$j(".user-block-hide").slideToggle(300);
$j("a.readon").toggleClass("down");
}
);
});
// make a random ungluing definition active onload
$j(document).ready(function() {
var length = $j(".block-intro-text div").length;
var ran = Math.floor(Math.random()*length)+1;
$j(".block-intro-text div:nth-child(" + ran + ")").attr('id', 'active');
});
// change the ungluing def onclick
$j(document).delegate(".block-intro-text", "click", function() {
var length = $j(".block-intro-text div").length;
// minus one because length includes THIS div and we want the set of its siblings only
var ran = Math.floor(Math.random()*length)-1;
// make sure our next active div is not the current one!
$j(this).children("#active").siblings().eq(ran).attr('id', 'foo');
$j(this).children("#active").removeAttr('id');
$j(this).children("#foo").attr('id', 'active');
});

View File

@ -249,7 +249,7 @@ ul.navigation li a:hover, ul.navigation li.active a {
padding: 5px;
}
}
.booklist-status-label, .right_add {
.booklist-status-label, {
display: none;
}
}

View File

@ -152,15 +152,13 @@
bottom: 5px;
}
.right_add {
float: right;
padding:10px 10px 0 0;
width:24px;
}
.panelnope {
display: none;
}
.rounded {
margin-bottom: 7px;
}
}
.panelview.boolist-ebook a {
@ -266,7 +264,7 @@ div.panelview.side2 {
/* title, author */
.white_text {
width:130px;
width:120px;
height:45px;
padding:15px 0px;
margin:0px;
@ -323,13 +321,6 @@ div.panelview.side2 {
float:left;
}
.right_add{
padding:10px;
margin:0px;
float:right;
}
p.right_add { float:right; padding:10px 10px 0 0; width:24px; }
/**/
.read2{
margin: 15px auto;

View File

@ -303,6 +303,10 @@ div.content-block-content {
iframe {
.mediaborder;
}
form {
margin-left: -5px;
}
}
ul.social li {
@ -361,21 +365,20 @@ ul.support li {
}
}
.work_supporter {
.work_supporter_nocomment {
height: 50px;
margin-top: 5px;
vertical-align: middle;
margin-left:-5px;
}
.work_supporter_avatar {
.work_supporter_avatar {
float: left;
}
}
.work_supporter_name {
.work_supporter_name {
.height(50px);
float: left;
margin-left: 5px;
}
}
/* this line differs from sitewide.css. should it? */
@ -426,3 +429,18 @@ a{ color:#3d4e53; font-size:12px;}
text-decoration: underline;
}
}
.thank-you {
font-size: 20px;
}
.work_supporter {
height: auto;
min-height: 50px;
margin-top: 5px;
vertical-align: middle;
margin-left:-5px;
}
.work_supporter_avatar {
margin-right: 5px;
}

View File

@ -1,17 +1,12 @@
// Styles basedocumentation.html and its descendants.
@import "variables.less";
@import "learnmore.less";
// Learn More area
/* Learn More area (not already styles by learnmore.less) */
.user-block {
width:100%; clear:both;
}
.user-block-hide {
float: left;
width:100%;
clear:both;
}
.user-block1, .user-block2 {
float:left;
}
@ -52,6 +47,10 @@
}
}
.user-block-hide .quicktour.last {
background: none;
}
/* Containers */
.have-right #js-main-container {
float: left;
@ -134,3 +133,7 @@ dd {
.errorlist li {
border: solid @call-to-action 4px;
}
.collapse ul {
display: none;
}

View File

@ -1,4 +1,5 @@
@import "variables.less";
@import "learnmore.less";
.clickyarrows() {
text-indent:-10000px;
@ -48,42 +49,6 @@
clear:both;
}
.user-block-hide {
float: left;
width:100%;
clear:both;
border-top: solid 1px @bright-blue;
margin-top: 20px;
.quicktour {
width: 270px;
float: left;
font-style: italic;
line-height:20px;
font-size:13px;
margin-top: 20px;
.highlight {
font-weight: bold;
}
&.last {
padding-right:0px;
width:270px;
background: url("@{image-base}landingpage/signmeup-arrow.png") no-repeat center bottom;
padding-bottom: 42px;
}
}
.movingrightalong {
background: url("@{image-base}landingpage/quicktour-arrow.png") no-repeat center;
height: 100px;
width: 75px;
float: left;
margin-top: 20px;
}
}
.user-block1, .user-block2 {
float:left;
}

View File

@ -0,0 +1,47 @@
@import "variables.less";
.user-block-hide {
float: left;
width:100%;
clear:both;
border-top: solid 1px @bright-blue;
margin-top: 20px;
.quicktour {
width: 270px;
float: left;
font-style: italic;
line-height:20px;
font-size:13px;
margin-top: 20px;
.highlight {
font-weight: bold;
}
&.last {
padding-right:0px;
width:270px;
background: url("@{image-base}landingpage/signmeup-arrow.png") no-repeat center bottom;
padding-bottom: 42px;
}
}
.movingrightalong {
background: url("@{image-base}landingpage/quicktour-arrow.png") no-repeat center;
height: 100px;
width: 75px;
float: left;
margin-top: 20px;
}
}
.block-intro-text div {
display: none;
line-height: 25px;
&#active {
display: inherit;
height: 75px;
}
}

View File

@ -96,6 +96,15 @@ ul.menu{
margin:0;
}
/* Learn More menu */
.block-intro-text {
padding-right: 10px;
span.def {
font-style: italic;
}
}
a.readon {
background:url("@{image-base}learnmore-downarrow.png") right center no-repeat;
color:#fff;

0
test/__init__.py Normal file
View File

View File

@ -1,8 +1,40 @@
from regluit.core import models
from regluit.payment.models import Transaction
from regluit.payment.models import Transaction, PaymentResponse, Receiver
from regluit.payment.manager import PaymentManager
from regluit.payment.paypal import IPN_PAY_STATUS_ACTIVE, IPN_PAY_STATUS_INCOMPLETE, IPN_PAY_STATUS_COMPLETED
def run_google_rc():
from selenium import selenium
import unittest, time, re
class google_rc(unittest.TestCase):
def setUp(self):
self.verificationErrors = []
self.selenium = selenium("localhost", 4444, "*firefox", "https://www.google.com/")
self.selenium.start()
def test_google_rc(self):
sel = self.selenium
sel.open("/")
sel.type("id=lst-ib", "Bach")
sel.click("name=btnG")
time.sleep(3)
try: self.failUnless(sel.is_text_present("Wikipedia"))
except AssertionError, e: self.verificationErrors.append(str(e))
def tearDown(self):
self.selenium.stop()
self.assertEqual([], self.verificationErrors)
testcases = [google_rc]
suites = unittest.TestSuite([unittest.TestLoader().loadTestsFromTestCase(testcase) for testcase in testcases])
unittest.TextTestRunner().run(suites)
# from selenium import webdriver
# driver = webdriver.Remote(desired_capabilities=webdriver.DesiredCapabilities.HTMLUNITWITHJS)
# driver.get("http://google.com")
pm = PaymentManager()
def campaign_display():
@ -31,7 +63,10 @@ def finish_campaigns(clist):
return [pm.finish_campaign(c) for c in clist]
def drop_all_transactions():
PaymentResponse.objects.all().delete()
Receiver.objects.all().delete()
Transaction.objects.all().delete()
# go through all Campaigns and set the self.left = self.target
for c in models.Campaign.objects.all():
c.left = c.target