From f18ab4c4c70b29751c3a82972e5aad8e77332bc8 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Mon, 31 Jul 2017 17:17:58 -0400 Subject: [PATCH] remove dead apps: poll_and_call, predictionmarket, whipturk Removes the dependencies on twilio libraries. Fixes #132. --- .env.server.template | 2 - bill/management/commands/make_markets.py | 111 ------ bill/models.py | 24 -- bill/urls.py | 2 - bill/views.py | 113 ------- build/pipreq.txt | 3 - poll_and_call/__init__.py | 0 poll_and_call/admin.py | 23 -- poll_and_call/management/__init__.py | 0 poll_and_call/management/commands/__init__.py | 0 .../management/commands/dump_calls.py | 104 ------ poll_and_call/models.py | 160 --------- poll_and_call/urls.py | 15 - poll_and_call/views.py | 318 ------------------ predictionmarket/__init__.py | 0 predictionmarket/admin.py | 7 - predictionmarket/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/top_markets.py | 23 -- predictionmarket/models.py | 285 ---------------- predictionmarket/urls.py | 9 - predictionmarket/views.py | 24 -- settings.py | 6 - settings_env.py | 2 - templates/bill/bill_details.html | 24 -- urls.py | 3 - whipturk/__init__.py | 0 whipturk/admin.py | 9 - whipturk/models.py | 99 ------ whipturk/urls.py | 11 - whipturk/views.py | 293 ---------------- 31 files changed, 1670 deletions(-) delete mode 100644 bill/management/commands/make_markets.py delete mode 100644 poll_and_call/__init__.py delete mode 100644 poll_and_call/admin.py delete mode 100644 poll_and_call/management/__init__.py delete mode 100644 poll_and_call/management/commands/__init__.py delete mode 100644 poll_and_call/management/commands/dump_calls.py delete mode 100644 poll_and_call/models.py delete mode 100644 poll_and_call/urls.py delete mode 100644 poll_and_call/views.py delete mode 100644 predictionmarket/__init__.py delete mode 100644 predictionmarket/admin.py delete mode 100644 predictionmarket/management/__init__.py delete mode 100644 predictionmarket/management/commands/__init__.py delete mode 100644 predictionmarket/management/commands/top_markets.py delete mode 100644 predictionmarket/models.py delete mode 100644 predictionmarket/urls.py delete mode 100644 predictionmarket/views.py delete mode 100644 whipturk/__init__.py delete mode 100644 whipturk/admin.py delete mode 100644 whipturk/models.py delete mode 100644 whipturk/urls.py delete mode 100644 whipturk/views.py diff --git a/.env.server.template b/.env.server.template index 65e71460..76d30a73 100644 --- a/.env.server.template +++ b/.env.server.template @@ -83,5 +83,3 @@ FACEBOOK_APP_ID=fill this in FACEBOOK_APP_SECRET=fill this in FACEBOOK_AUTH_SCOPE= -TWILIO_ACCOUNT_SID=fill this in -TWILIO_AUTH_TOKEN=fill this in \ No newline at end of file diff --git a/bill/management/commands/make_markets.py b/bill/management/commands/make_markets.py deleted file mode 100644 index 5bbf45da..00000000 --- a/bill/management/commands/make_markets.py +++ /dev/null @@ -1,111 +0,0 @@ -from django.core.management.base import BaseCommand, CommandError -from django.conf import settings -from django.contrib.contenttypes.models import ContentType -from django.contrib.auth.models import User -from django.conf import settings - -from optparse import make_option - -from bill.models import Bill, BillStatus -from predictionmarket.models import Market, Outcome, Trade, TradingAccount -from bill.prognosis import compute_prognosis - -from math import log - -class Command(BaseCommand): - args = '' - help = 'Make and update prediction markets.' - - def handle(self, *args, **options): - bank = TradingAccount.get(User.objects.get(id=settings.PREDICTIONMARKET_BANK_UID)) - bill_ct = ContentType.objects.get_for_model(Bill) - - # For every bill, make a market for its next major step and close any other markets. - for bill in Bill.objects.filter(congress=settings.CURRENT_CONGRESS): - market_key = None # current market to open - market_name = None # name of the market to open - market_outcomes = None # dict of outcome names - market_close = { } # markets to close, key is market id and value is the key of the winning outcome - - if bill.current_status in (BillStatus.introduced, BillStatus.referred, BillStatus.reported): - market_key = 0 - market_name = "Will %s pass the %s?" % (bill.display_number, bill.originating_chamber) - market_outcomes = { 0: "No", 1: "Yes" } - market_close = { } - elif bill.current_status in (BillStatus.pass_over_house, BillStatus.pass_over_senate): - market_key = 1 - market_name = "Will %s pass the %s?" % (bill.display_number, bill.opposite_chamber) - market_outcomes = { 0: "No", 1: "Yes" } - market_close = { 0: 1 } # originating chamber passed - elif bill.current_status in (BillStatus.pass_back_house, BillStatus.pass_back_senate): - market_key = 2 - market_name = "Will %s pass in identical form in the House and Senate?" % bill.display_number - market_outcomes = { 0: "No", 1: "Yes" } - market_close = { 0: 1, 1: 1 } # originating chamber passed, other chamber passed - elif bill.current_status in (BillStatus.fail_originating_house, BillStatus.fail_originating_senate): - market_close = { 0: 0 } # originating chamber failed - elif bill.current_status in (BillStatus.fail_second_house, BillStatus.fail_second_senate): - market_close = { 0: 1, 1: 0 } # originating chamber passed, second chamber failed - elif bill.current_status in (BillStatus.passed_constamend, BillStatus.passed_concurrentres, BillStatus.passed_bill, - BillStatus.override_pass_over_house, BillStatus.override_pass_over_senate, BillStatus.vetoed_pocket, - BillStatus.vetoed_override_fail_originating_house, BillStatus.vetoed_override_fail_originating_senate, - BillStatus.vetoed_override_fail_second_house, BillStatus.vetoed_override_fail_second_senate, - BillStatus.enacted_signed, BillStatus.enacted_veto_override): - market_close = { 0: 1, 1: 1 } # originating chamber passed, other chamber passed - elif bill.current_status in (BillStatus.passed_simpleres,): - market_close = { 0: 1 } # originating chamber passed - else: - # Don't know what to do in this state, so just keep whatever we had before. - continue - - did_see_market = False - for market in Market.objects.filter(owner_content_type=bill_ct, owner_object_id=bill.id, isopen=True): - if int(market.owner_key) == market_key: - did_see_market = True - elif int(market.owner_key) in market_close: - print "Closing market:", market - market.close_market() # do this before the next check to make sure no trades slip in at the last moment - if Trade.objects.filter(outcome__market=market).exclude(account=bank).count() == 0: - print "\tDeleting market because the only trader was the bank." - market.delete() # TODO: Leaves the bank's account balance alone, which is not really good. - else: - for outcome in market.outcomes.all(): - outcome.liquidate(1.0 if market_close[int(market.owner_key)] == int(outcome.owner_key) else 0.0) - else: - print "Don't know what to do with market:", market, market.owner_key - - if not did_see_market and market_key != None: - starting_price = compute_prognosis(bill)["prediction"] / 100.0 - - # Create the market. - m = Market() - m.owner_object = bill - m.owner_key = market_key - m.name = market_name - m.volatility = 200.0 # large enough so that an integer number of shares can yield .01 precision in pricing - m.save() - ocmap = { } - for k, v in market_outcomes.items(): - o = Outcome() - o.market = m - o.owner_key = k - o.name = v - o.save() - ocmap[k] = o - - # The bank buys enough shares to make the starting price match our bill prognosis. - # Since we have two outcomes and the yes-price is exp(q1/b) / (exp(q1/b) + exp(q2/b)) - # then.... - if starting_price < .01: starting_price = .01 - if starting_price > .99: starting_price = .99 - shares = int(round(m.volatility * log(starting_price / (1.0 - starting_price)))) - t = None - if starting_price > .5 and shares > 0: - t = Trade.place(bank, ocmap[1], shares, check_balance=False) - elif starting_price < .5 and shares < 0: - t = Trade.place(bank, ocmap[0], -shares, check_balance=False) - - print "Created market", m - if t: - print "\twith", t.shares, "of", t.outcome - diff --git a/bill/models.py b/bill/models.py index 1ed7e117..a0058feb 100644 --- a/bill/models.py +++ b/bill/models.py @@ -1354,30 +1354,6 @@ def date_filter(d): return None - def get_open_market(self, user): - from django.contrib.contenttypes.models import ContentType - bill_ct = ContentType.objects.get_for_model(Bill) - - import predictionmarket.models - try: - m = predictionmarket.models.Market.objects.get(owner_content_type=bill_ct, owner_object_id=self.id, isopen=True) - except predictionmarket.models.Market.DoesNotExist: - return None - - for outcome in m.outcomes.all(): - if outcome.owner_key == "1": # "yes" - m.yes = outcome - m.yes_price = int(round(outcome.price() * 100.0)) - if user and user.is_authenticated(): - account = predictionmarket.models.TradingAccount.get(user, if_exists=True) - if account: - positions, profit = account.position_in_market(m) - m.user_profit = round(profit, 1) - m.user_positions = { } - for outcome in positions: - m.user_positions[outcome.owner_key] = positions[outcome] - return m - def get_gop_summary(self): import urllib, StringIO try: diff --git a/bill/urls.py b/bill/urls.py index 958ccc2d..e5748ec9 100644 --- a/bill/urls.py +++ b/bill/urls.py @@ -15,11 +15,9 @@ url(r'^$', 'bill_docket', name='bill_docket'), url(r'^statistics$', 'bill_statistics', name='bill_stats'), url(r'^uscode(?:/(\d+|.*))?$', 'uscodeindex'), - url(r'^_ajax/market_test_vote', 'market_test_vote'), url(r'^_ajax/load_text', 'bill_text_ajax'), url(r'^_ajax/join_community', 'join_community'), url(r'^_admin/go_to_summary_admin', 'go_to_summary_admin', name="bill_go_to_summary_admin"), - url(r'^_redirect/start-poll', 'start_poll', name="bill_start_poll"), url(r'^(\d+)/([a-z]+)(\d+)/(thumbnail|_text_image)$', 'bill_text_image'), ) diff --git a/bill/views.py b/bill/views.py index cb71a4c1..1464d0ba 100644 --- a/bill/views.py +++ b/bill/views.py @@ -105,51 +105,18 @@ def bill_details_user_view(request, congress, type_slug, number): | Edit
Tracked by {{feed.tracked_in_lists.count|intcomma}} users ({{feed.tracked_in_lists_with_email.count|intcomma}} w/ email). -
{{num_issuepos}} poll responses, {{num_calls}} phone calls to Congress. """ - from poll_and_call.models import RelatedBill as IssueByBill - try: - from poll_and_call.models import * - ix = RelatedBill.objects.get(bill=bill).issue - num_issuepos = UserPosition.objects.filter(position__issue=ix).count() - num_calls = len([c for c in CallLog.objects.filter(position__position__issue=ix) if c.is_complete()]) - except IssueByBill.DoesNotExist: - num_issuepos = 0 - num_calls = 0 - from django.template import Template, Context, RequestContext, loader ret["admin_panel"] = Template(admin_panel).render(RequestContext(request, { 'bill': bill, "feed": bill.get_feed(), - "num_issuepos": num_issuepos, - "num_calls": num_calls, })) from person.views import render_subscribe_inline ret.update(render_subscribe_inline(request, bill.get_feed())) - # poll_and_call - if request.user.is_authenticated(): - from poll_and_call.models import RelatedBill as IssueByBill, UserPosition - try: - issue = IssueByBill.objects.get(bill=bill).issue - try: - up = UserPosition.objects.get(user=request.user, position__issue=issue) - targets = up.get_current_targets() - ret["poll_and_call_position"] = { - "id": up.position.id, - "text": up.position.text, - "can_change": up.can_change_position(), - "can_call": [(t.id, t.person.name) for t in targets] if isinstance(targets, list) else [], - "call_url": issue.get_absolute_url() + "/make_call", - } - except UserPosition.DoesNotExist: - pass - except IssueByBill.DoesNotExist: - pass - # emoji reactions import json from website.models import Reaction @@ -244,30 +211,6 @@ def bill_widget_info(request, congress, type_slug, number): "SITE_ROOT_URL": settings.SITE_ROOT_URL, } -@json_response -@login_required -def market_test_vote(request): - bill = get_object_or_404(Bill, id = request.POST.get("bill", "0")) - prediction = int(request.POST.get("prediction", "0")) - market = bill.get_open_market(request.user) - if not market: return { } - - from predictionmarket.models import Trade, TradingAccount - account = TradingAccount.get(request.user) - if prediction != 0: - # Buy shares in one of the outcomes. - try: - t = Trade.place(account, market.outcomes.get(owner_key = 1 if prediction == 1 else 0), 10) - except ValueError as e: - return { "error": str(e) } - - else: - # Sell shares. - positions, pl = account.position_in_market(market) - for outcome in positions: - Trade.place(account, outcome, -positions[outcome]["shares"]) - - return { "vote": prediction } @anonymous_view @render_to('bill/bill_text.html') @@ -753,62 +696,6 @@ def uscodeindex(request, secid): "feed": (Feed.objects.get_or_create(feedname="usc:" + str(parent.id))[0]) if parent else None, } -@anonymous_view -def start_poll(request): - from poll_and_call.models import Issue, IssuePosition, RelatedBill as IssueByBill - - # get the bill & valence - bill = get_object_or_404(Bill, id=request.GET.get("bill")) - valence = (request.GET.get("position") == "support") - - # get the Issue - try: - ix = IssueByBill.objects.get(bill=bill).issue - except IssueByBill.DoesNotExist: - # no Issue yet, so create - ix = Issue.objects.create( - slug = "%d-%s-%d" % (bill.congress, bill.bill_type_slug, bill.number), - title = "PLACEHOLDER", - question = "PLACEHOLDER", - introtext = "Weigh in on %s." % bill.display_number_with_congress_number, - isopen = True, - ) - IssueByBill.objects.create(issue=ix, bill=bill, valence=True) - - # update the Issue since the bill title may have changed - ix.title = bill.title - ix.question = "What is your position on %s?" % bill.title - ix.save() - - # how to refer to the bill in the call script - from django.template.defaultfilters import truncatewords - title = bill.title_no_number - if re.match(".* Act( of \d{4})?", title): - title = "The " + title - title = bill.display_number + ": " + title - bt = truncatewords(title, 11) - if "..." in bt: - bt = truncatewords(title, 15) - bt = u"%s (\u201C%s\u201D)" % (bill.display_number, bt.replace(bill.display_number + ": ", "")) - - # create and update the options - for opt_valence, opt_verb in ((True, "support"), (False, "oppose")): - try: - p = ix.positions.get(valence=opt_valence) - except: - p = IssuePosition.objects.create( - text="PLACEHOLDER", - valence=opt_valence, - call_script="PLACEHOLDER", - ) - ix.positions.add(p) - - p.text = opt_verb.title() - p.call_script = "I %s %s." % (opt_verb, bt) - p.save() - - return HttpResponseRedirect(ix.get_absolute_url() + "/join/" + str(ix.positions.get(valence=valence).id)) - @anonymous_view def bill_text_image(request, congress, type_slug, number, image_type): bill = load_bill_from_url(congress, type_slug, number) diff --git a/build/pipreq.txt b/build/pipreq.txt index 556ae52b..e1766895 100644 --- a/build/pipreq.txt +++ b/build/pipreq.txt @@ -16,13 +16,10 @@ recaptcha-client django-wysiwyg rcssmin markdown2 -django_twilio -twilio<6.0.0 tqdm pyyaml pynliner git+https://github.com/govtrack/django-lorien-common -phonenumbers git+https://github.com/if-then-fund/django-html-emailer@v0.0.2 xml_diff diff_match_patch_python diff --git a/poll_and_call/__init__.py b/poll_and_call/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/poll_and_call/admin.py b/poll_and_call/admin.py deleted file mode 100644 index bfba1dfc..00000000 --- a/poll_and_call/admin.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 - -from django.contrib import admin -from poll_and_call.models import Issue, IssuePosition, RelatedBill, UserPosition, CallLog - -class IssueAdmin(admin.ModelAdmin): - prepopulated_fields = {"slug": ("title",)} - raw_id_fields = ['positions'] - -class RelatedBillAdmin(admin.ModelAdmin): - raw_id_fields = ['issue', 'bill'] - -class UserPositionAdmin(admin.ModelAdmin): - raw_id_fields = ['user', 'position'] - -class CallLogAdmin(admin.ModelAdmin): - raw_id_fields = ['user', 'position', 'target'] - -admin.site.register(Issue, IssueAdmin) -admin.site.register(IssuePosition) -admin.site.register(RelatedBill, RelatedBillAdmin) -admin.site.register(UserPosition, UserPositionAdmin) -admin.site.register(CallLog, CallLogAdmin) diff --git a/poll_and_call/management/__init__.py b/poll_and_call/management/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/poll_and_call/management/commands/__init__.py b/poll_and_call/management/commands/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/poll_and_call/management/commands/dump_calls.py b/poll_and_call/management/commands/dump_calls.py deleted file mode 100644 index 48546106..00000000 --- a/poll_and_call/management/commands/dump_calls.py +++ /dev/null @@ -1,104 +0,0 @@ -from django.core.management.base import BaseCommand, CommandError -from django.conf import settings -from django.db.models import Max - -import csv, sys -from random import shuffle - -from website.models import UserProfile -from poll_and_call.models import * -from person.name import get_person_name - -# cache -max_research_anon_key = UserProfile.objects.aggregate(n=Max('research_anon_key'))['n'] or 0 - -class Command(BaseCommand): - - def handle(self, *args, **options): - # get all calls - calls = list(CallLog.objects.all() - .filter(position__position__issue__bills__id__gt=0) # only calls on bills, since we had a few non-bill calls at the beginning - .exclude(user__email__endswith="@govtrack.us").exclude(user__email__endswith="@occams.info") # no testing calls by me - .order_by('created') - ) - - # filter - take only calls with recordings - calls = filter(lambda call : call.log.get("finished", {}).get("RecordingUrl"), calls) - - ## shuffle so new anonymous ID assignments are random - #shuffle(calls) - - # write out - w = csv.writer(sys.stdout) - - w.writerow([ - "call_id", - "call_date", - "call_duration", - # "recording_url", # !!! not-anonymized - - "caller_id", - "caller_account_created", - - "caller_district", - "caller_areacode", - "caller_areacode_city", - "caller_areacode_state", - - "topic_id", - "topic_name", - "topic_link", - "position_id", - "position_name", - - "office_id", - "office_type", - "office_name", - ]) - - def anonymize_user(user): - global max_research_anon_key - profile = user.userprofile() - if profile.research_anon_key is None: - # assign next available ID - max_research_anon_key += 1 - profile.research_anon_key = max_research_anon_key - profile.save() - return profile.research_anon_key - - for call in calls: - if len(call.position.district) == 2: continue # data error in our very early data (one record?) - - # for making the right name for the called office - call.target.person.role = call.target - - # write row - w.writerow([ - # call - call.id, - call.created.isoformat(), - int(call.log["finished"]["RecordingDuration"][0]), - # !!! this is not anonymous - call.log["finished"]["RecordingUrl"][0], - - # user - anonymize_user(call.user), - call.user.date_joined.isoformat(), - - # caller - call.position.district, - call.log["start"]["Called"][0][2:5], - call.log["start"]["CalledCity"][0], - call.log["start"]["CalledState"][0], - - # topic of call - call.position.position.issue.first().id, - call.position.position.issue.first().title.encode("utf8"), - ";".join(["https://www.govtrack.us"+b.bill.get_absolute_url() for b in call.position.position.issue.first().bills.all()]), - call.position.position.id, - call.position.position.text.encode("utf8"), - - # target of call - call.target.person.id, - call.target.get_title(), - get_person_name(call.target.person).encode("utf8"), - ]) diff --git a/poll_and_call/models.py b/poll_and_call/models.py deleted file mode 100644 index 5c5250cf..00000000 --- a/poll_and_call/models.py +++ /dev/null @@ -1,160 +0,0 @@ -# -*- coding: utf-8 -*- -from django.db import models -from jsonfield import JSONField - -import random, datetime - -class Issue(models.Model): - """An issue is something that users might want to weigh in on.""" - - slug = models.SlugField(help_text="The slug for this issue in URLs.") - title = models.CharField(max_length=255, help_text="The issue's display title.") - question = models.CharField(max_length=255, help_text="The issue phrased as a question.") - introtext = models.TextField(help_text="Text introducing the issue.") - positions = models.ManyToManyField('IssuePosition', db_index=True, related_name="issue", help_text="The positions associated with this issue.") - created = models.DateTimeField(auto_now_add=True, db_index=True, help_text="The date and time the issue was created.") - isopen = models.BooleanField(default=False, verbose_name="Open", help_text="Whether users can currently participate in this issue.") - - class Meta: - ordering = ('-created',) - - def __unicode__(self): - return self.title + "/" + self.question - - def get_absolute_url(self): - return "/poll/" + self.slug - - def get_randomized_positions(self): - # Randomize the positions because that's better for polling. - p = list(self.positions.all()) - random.shuffle(p) - return p - -class IssuePosition(models.Model): - """A position that a user can take on an issue.""" - - text = models.CharField(max_length=255, help_text="A description of the position.") - valence = models.NullBooleanField(blank=True, null=True, help_text="The valence of this position, for linking with bills.") - created = models.DateTimeField(auto_now_add=True, db_index=True, help_text="The date and time the issue was created.") - call_script = models.TextField(blank=True, null=True, help_text="What you should say when you call your rep about this issue.") - - class Meta: - ordering = ('-created',) - - def __unicode__(self): - i = "?" - try: - i = self.issue.all()[0].title - except: - pass - v = "" - if self.valence is True: v = "(+) " - if self.valence is False: v = "(-) " - return v + i + " -- " + self.text - -class RelatedBill(models.Model): - """A bill related to an issue, and possibly a link between support/oppose for the bill and IssuePositions.""" - - issue = models.ForeignKey(Issue, db_index=True, related_name="bills", help_text="The related issue.", on_delete=models.CASCADE) - bill = models.ForeignKey('bill.Bill', db_index=True, help_text="The related bill.", on_delete=models.PROTECT) - valence = models.NullBooleanField(blank=True, null=True, help_text="The valence of this bill, for linking with IssuePositions. If not null, a user who supports this bill takes the position of the IssuePosition with the same valence value.") - - def __unicode__(self): - v = "" - if self.valence is True: v = "(+) " - if self.valence is False: v = "(-) " - return v + self.issue.title + " -- " + unicode(self.bill) - -class UserPosition(models.Model): - """The position of a user on an issue.""" - - user = models.ForeignKey('auth.User', db_index=True, help_text="The user who created this position.", on_delete=models.CASCADE) - position = models.ForeignKey(IssuePosition, db_index=True, help_text="The position the user choses.", on_delete=models.CASCADE) - created = models.DateTimeField(auto_now_add=True, db_index=True) - - district = models.CharField(max_length=4, db_index=True, help_text="The state and district, in uppercase without any spaces, of the user at the time the user took this posiiton.") - - metadata = JSONField(help_text="Other information stored with the position.") - - def __unicode__(self): - return self.created.isoformat() + " " + unicode(self.user) + "/" + unicode(self.position) - - def can_change_position(self): - # Can the user change his position? Not if he made any calls about it. - return not CallLog.objects.filter(position=self).exists() - - def get_current_targets(self): - # Returns either a list of a string with a reason why there are no targets to call. - - # ugh, data collection error when this was first launched - if len(self.district) <= 2: return "unknown" - - # rate-limit user activity - if CallLog.objects.filter(user=self.user, created__gt=datetime.datetime.now()-datetime.timedelta(days=10)).count() >= 15 \ - or CallLog.objects.filter(user=self.user, created__gt=datetime.datetime.now()-datetime.timedelta(days=1)).count() >= 6: - return "rate-limit" - - # get all of the chambers any related bills are currently being considered in - issue = self.position.issue.get() - chambers = set() - for rb in RelatedBill.objects.filter(issue=issue).select_related("bill"): - chambers.add(rb.bill.current_status_chamber) - - # get the represenative or senators as appropriate, and check for various - # error conditions along the way. - from person.models import PersonRole, RoleType - from us import stateapportionment - targets = [] - if len(chambers) == 0 or "House" in chambers or "Unknown" in chambers: - targets.extend( PersonRole.objects.filter(current=True, role_type=RoleType.representative, state=self.district[0:2], district=int(self.district[2:])) ) - if len(targets) == 0 and ((len(chambers) == 1 and "House" in chambers) or stateapportionment[self.district[0:2]] == "T"): return "house-vacant" - if len(chambers) == 0 or "Senate" in chambers or "Unknown" in chambers: - targets.extend( PersonRole.objects.filter(current=True, role_type=RoleType.senator, state=self.district[0:2]) ) - if len(chambers) == 1 and "Senate" in chambers and len(targets) == 0: - if stateapportionment[self.district[0:2]] == "T": return "no-senators" - return "senate-vacant" - if len(targets) == 0: return "vacant" - - # make sure we have a phone number on file - def is_valid_phone(phone): - if not phone: return False - if len("".join(c for c in phone if unicode.isdigit(c))) != 10: return False - return True - targets = [t for t in targets if is_valid_phone(t.phone)] - if len(targets) == 0: return "no-phone" - - # filter out anyone the user has already called - targets = [t for t in targets if not CallLog.has_made_successful_call(self, t)] - if len(targets) == 0: return "all-calls-made" - - return targets - - -class CallLog(models.Model): - """The log of a call to Congress.""" - - user = models.ForeignKey('auth.User', db_index=True, help_text="The user who created this call.", on_delete=models.CASCADE) - position = models.ForeignKey(UserPosition, db_index=True, help_text="The position this call was communicating.", on_delete=models.CASCADE) - target = models.ForeignKey('person.PersonRole', db_index=True, help_text="The Member of Congress the user called.", on_delete=models.PROTECT) - created = models.DateTimeField(auto_now_add=True, db_index=True) - - status = models.CharField(max_length=64) # current status of the call - - log = JSONField(help_text="A dict of TwilML information for different parts of the call.") - - class Meta: - ordering = ('-created',) - - def __unicode__(self): - return self.created.isoformat() + " " + unicode(self.user) + " " + unicode(self.position) - - def is_complete(self): - return isinstance(self.log, dict) and self.log.get("finished", {}).get("RecordingUrl") is not None - - @staticmethod - def has_made_successful_call(userposition, target): - for cl in CallLog.objects.filter(position=userposition, target=target): - if cl.is_complete(): - return True - return False - diff --git a/poll_and_call/urls.py b/poll_and_call/urls.py deleted file mode 100644 index 9e13ce11..00000000 --- a/poll_and_call/urls.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -from django.conf.urls import * - -urlpatterns = patterns('poll_and_call.views', - url(r'^([a-z0-9\-\_]+)$', 'issue_show'), - url(r'^([a-z0-9\-\_]+)/join/(\d+)$', 'issue_join'), - url(r'^([a-z0-9\-\_]+)/make_call$', 'issue_make_call'), - url(r'^_ajax/start-call$', 'start_call'), - url(r'^_ajax/poll-call-status$', 'poll_call_status'), - url(r'^_twilio/call-start/(?P\d+)$', 'twilio_call_start'), - url(r'^_twilio/call-input/(?P\d+)$', 'twilio_call_input'), - url(r'^_twilio/call-transfer-end/(?P\d+)$', 'twilio_call_transfer_ended'), - url(r'^_twilio/call-end/(?P\d+)$', 'twilio_call_end'), -) - diff --git a/poll_and_call/views.py b/poll_and_call/views.py deleted file mode 100644 index 087298e1..00000000 --- a/poll_and_call/views.py +++ /dev/null @@ -1,318 +0,0 @@ -# -*- coding: utf-8 -*- -from django.http import Http404, HttpResponseRedirect, HttpResponse -from django.shortcuts import redirect, get_object_or_404 -from django.core.urlresolvers import reverse -from django.contrib.auth.decorators import login_required -from django.conf import settings -from django.db.models import Count, F - -from common.decorators import render_to - -from poll_and_call.models import Issue, IssuePosition, UserPosition, CallLog -from person.models import PersonRole - -from twostream.decorators import anonymous_view, user_view_for -from registration.helpers import json_response - -from datetime import datetime - -@anonymous_view -@render_to('poll_and_call/issue_details.html') -def issue_show(request, issue_slug): - issue = get_object_or_404(Issue, slug=issue_slug) - - # get the positions, and map ID to the position object - positions = issue.positions.all() - pos_map = dict({ p.id: p for p in positions }) - - # get the current poll results (set default values first) - for p in positions: - p.total_users = 0 - p.percent_users = 0 - results = UserPosition.objects.filter(position__issue=issue).values("position").annotate(count=Count('id')) - total_users = sum(r["count"] for r in results) - for r in results: - p = pos_map[r["position"]] - p.total_users = r["count"] - p.percent_users = int(round(100.0*r["count"]/total_users)) - - return { "issue": issue, "positions": positions, "total_users": total_users } - -@user_view_for(issue_show) -def issue_show_user_view(request, issue_slug): - issue = get_object_or_404(Issue, slug=issue_slug) - D = { } - if request.user.is_authenticated(): - try: - up = UserPosition.objects.get(user=request.user, position__issue=issue) - targets = up.get_current_targets() - D["position"] = { - "id": up.position.id, - "text": up.position.text, - "can_change": up.can_change_position(), - "can_call": [(t.id, t.person.name) for t in targets] if isinstance(targets, list) else [], - } - except: - pass - return D - -@login_required -@render_to('poll_and_call/issue_join.html') -def issue_join(request, issue_slug, position_id): - issue = get_object_or_404(Issue, slug=issue_slug) - try: - position = issue.positions.get(id=position_id) - except IssuePosition.DoesNotExist: - raise Http404() - - try: - up = UserPosition.objects.get(user=request.user, position__issue=issue) - if up.can_change_position(): - # User can change his position. Delete the old position before going on. - up.delete() - else: - # The position is fixed. Go on to making a call. - return HttpResponseRedirect(issue.get_absolute_url() + "/make_call") - except UserPosition.DoesNotExist: - pass - - if request.method == "POST": - # This is a confirmation. - - # Create the new position. - UserPosition.objects.create( - user=request.user, - position=position, - district=request.POST.get("district"), - metadata={ - "choice_display_index": request.GET.get("meta_order"), - }) - return HttpResponseRedirect(issue.get_absolute_url() + "/make_call") - - return { "issue": issue, "position": position } - -@login_required -@render_to('poll_and_call/issue_make_call.html') -def issue_make_call(request, issue_slug): - issue = get_object_or_404(Issue, slug=issue_slug) - user_position = get_object_or_404(UserPosition, user=request.user, position__issue=issue) - - # is there a representative to call in the user's district that the user hasn't called? - targets = user_position.get_current_targets() - if isinstance(targets, str): - return HttpResponseRedirect(issue.get_absolute_url() + "#" + targets) - - # choose a target at random - rep = targets[0] - - # if 'target' is a GET parameter, call that target. - try: - rep = [t for t in targets if t.id == int(request.GET['target'])][0] - except: - # target is not a parameter, is not an integer, is not one of the targets - pass - - # is this a good time of day to call? - #if not ((0 <= datetime.now().weekday() <= 4) and ('09:15' <= datetime.now().time().isoformat() <= '16:45')): - # return HttpResponseRedirect(issue.get_absolute_url() + "#hours") - - position = user_position.position - position.call_script = dynamic_call_script(issue, position, rep.person, rep) - - next_step = issue.get_absolute_url() - for bill in issue.bills.all(): - next_step = bill.bill.get_absolute_url() - - return { - "issue": issue, - "user_position": user_position, - "position": position, - "moc": rep, - "other_targets": [t for t in targets if t != rep], - "next_step": next_step, - } - -def dynamic_call_script(issue, position, person, role): - name = role.get_title() + " " + person.lastname - - from person.types import Gender - pos_pro = Gender.by_value(person.gender).pronoun_posessive - - if issue.slug == "gov-shutdown-2013" and position.valence is True: - if role.party == "Democrat": - return position.call_script + " I ask %s to be open to a repeal or delay of the Affordable Care Act." % (name,) - elif role.party == "Republican": - return position.call_script + " I ask %s to hold %s ground until the Democrats come to the table to negotiate." % (name, pos_pro) - elif issue.slug == "gov-shutdown-2013" and position.valence is False: - if role.party == "Democrat": - return position.call_script + " I ask %s to hold %s ground until the Republicans agree to fund the government." % (name, pos_pro) - elif role.party == "Republican": - return position.call_script + " I ask %s to pass a clean CR." % (name,) - elif issue.slug == "debt-ceiling-2013": - return position.call_script % (name,) - return position.call_script - -def twilio_client(): - from twilio.rest import TwilioRestClient - return TwilioRestClient(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN) - -@login_required -@json_response -def start_call(request): - user_position = get_object_or_404(UserPosition, id=request.POST["p"], user=request.user) - - # validate the 'target' parameter - possible_targets = user_position.get_current_targets() - if isinstance(possible_targets, str): return { "ok": False, "msg": "There is no one for you to call." } - try: - target = [t for t in possible_targets if t.id == int(request.POST['target'])][0] - except: - return { "ok": False, "msg": "You cannot call that office." } - - # basic phone number validation - phone_num = "".join(c for c in request.POST["phone_number"] if unicode.isdigit(c)).encode("ascii") - if len(phone_num) != 10: - return { "ok": False, "msg": "Enter your area code and phone number." } - - try: - client = twilio_client() - except Exception as e: - return { "ok": False, "msg": "Site error: " + str(e) } - - cl = CallLog.objects.create( - user=request.user, - position=user_position, - target=target, - status="not-yet-started", - log={}) - - call = client.calls.create( - to=("+1" + phone_num), - from_=settings.TWILIO_OUR_NUMBER, - url=request.build_absolute_uri("/poll/_twilio/call-start/" + str(cl.id)), - status_callback=request.build_absolute_uri("/poll/_twilio/call-end/" + str(cl.id)), - ) - - cl.log["sid"] = call.sid - cl.status = "started" - cl.save() - - return { "ok": True, "call_id": cl.id } - -# except Exception as e: -# return { "ok": False, "msg": "Something went wrong, sorry!", "error": repr(e) } - -@login_required -@json_response -def poll_call_status(request): - call_log = get_object_or_404(CallLog, id=request.POST["id"], user=request.user) - - if call_log.status == "not-yet-started": - msg = "Something went wrong, sorry!" - elif call_log.status == "started": - msg = "We are dialing your number..." - elif call_log.status == "picked-up": - msg = "You picked up. Dial 1 to be connected." - elif call_log.status == "connecting": - msg = "Connecting you to Congress..." - elif call_log.status == "connection-ended": - msg = "Ending the call..." - elif call_log.status == "ended": - msg = "Call ended." - - return { "finished": call_log.status == "ended", "msg": msg } - -from twilio.twiml import Response as TwilioResponse -from django_twilio.decorators import twilio_view - -def get_request_log_info(request): - log_meta_fields = ('REMOTE_ADDR', 'HTTP_COOKIE') - ret = { f: request.META.get(f) for f in log_meta_fields } - ret["time"] = datetime.now().isoformat() - return ret - -@twilio_view -def twilio_call_start(request, calllog_id): - call_log = get_object_or_404(CallLog, id=int(calllog_id)) - call_log.status = "picked-up" - call_log.log["start"] = dict(request.POST) - call_log.log["start"]["_request"] = get_request_log_info(request) - call_log.save() - - resp = TwilioResponse() - resp.say("Hello from Gov Track.") - g = resp.gather( - action=request.build_absolute_uri("/poll/_twilio/call-input/" + str(call_log.id)), - numDigits=1, - timeout=20, - ) - g.say("Press one to be connected to the office of %s %s. Press two if you did not request this call. Or simply hang up if you do not want your call to be connected." % ( - call_log.target.get_title(), - call_log.target.person.lastname)) - resp.say("Oooo too slow. We're going to hang up now.") - - return resp - -@twilio_view -def twilio_call_input(request, calllog_id): - call_log = get_object_or_404(CallLog, id=int(calllog_id)) - digit = request.POST["Digits"] - - call_log.log["input"] = dict(request.POST) - call_log.log["input"]["_request"] = get_request_log_info(request) - - resp = TwilioResponse() - - if digit != "1": - # basically an abuse report - call_log.log["input"]["response"] = "did-not-request-call" - resp.say("We apologize for the inconvenience. Call 202-558-7227 or visit w w w dot gov track dot u s to report abuse. Good bye.") - resp.hangup() - - elif settings.DEBUG: - resp.say("Site is in debug mode. Call cancelled.") - resp.hangup() - - else: - phone = "+1" + "".join(c for c in call_log.target.phone if unicode.isdigit(c)) - - call_log.log["input"]["response"] = "continue" - call_log.log["input"]["transfer_to"] = phone - - resp.say("Okay. Hold on.") - resp.dial( - phone, - action=request.build_absolute_uri("/poll/_twilio/call-transfer-end/" + str(call_log.id)), - timeout=30, - callerId=request.POST["To"], - record=True, - ) - - call_log.status = "connecting" - call_log.save() - - return resp - -@twilio_view -def twilio_call_transfer_ended(request, calllog_id): - call_log = get_object_or_404(CallLog, id=int(calllog_id)) - call_log.status = "connection-ended" - call_log.log["finished"] = dict(request.POST) - call_log.log["finished"]["_request"] = get_request_log_info(request) - call_log.save() - - resp = TwilioResponse() - resp.say("Your call to Congress has ended. Thank you for being a good citizen. Goodbye.") - return resp - -@twilio_view -def twilio_call_end(request, calllog_id): - call_log = get_object_or_404(CallLog, id=int(calllog_id)) - call_log.status = "ended" - call_log.log["end"] = dict(request.POST) - call_log.log["end"]["_request"] = get_request_log_info(request) - call_log.save() - - # empty response - resp = TwilioResponse() - return resp diff --git a/predictionmarket/__init__.py b/predictionmarket/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/predictionmarket/admin.py b/predictionmarket/admin.py deleted file mode 100644 index 3dd23971..00000000 --- a/predictionmarket/admin.py +++ /dev/null @@ -1,7 +0,0 @@ -from models import * -from django.contrib import admin - -admin.site.register(TradingAccount) -admin.site.register(Market) -admin.site.register(Outcome) - diff --git a/predictionmarket/management/__init__.py b/predictionmarket/management/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/predictionmarket/management/commands/__init__.py b/predictionmarket/management/commands/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/predictionmarket/management/commands/top_markets.py b/predictionmarket/management/commands/top_markets.py deleted file mode 100644 index be8af52e..00000000 --- a/predictionmarket/management/commands/top_markets.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.core.management.base import BaseCommand, CommandError -from django.conf import settings -from django.contrib.contenttypes.models import ContentType -from django.contrib.auth.models import User -from django.conf import settings - -from optparse import make_option - -from predictionmarket.models import Market, Outcome, TradingAccount - -class Command(BaseCommand): - args = '' - help = 'Shows statistics of the most active markets.' - - def handle(self, *args, **options): - bank = TradingAccount.get(User.objects.get(id=settings.PREDICTIONMARKET_BANK_UID)) - - for market in Market.objects.order_by('-tradecount')[0:10]: - print market - for outcome, price in market.prices().items(): - bank_shares = bank.positions(outcome=outcome).get(outcome, { "shares": 0 })["shares"] - print round(price*100, 1), outcome, "@", outcome.volume-bank_shares, "outstanding shares" - print diff --git a/predictionmarket/models.py b/predictionmarket/models.py deleted file mode 100644 index 068d9a0c..00000000 --- a/predictionmarket/models.py +++ /dev/null @@ -1,285 +0,0 @@ -from django.db import models, connection -from django.contrib.auth.models import User -from django.conf import settings -from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes import generic - -from math import exp, log - -class TradingAccount(models.Model): - """A user account holding the user's balance (i.e. money remaining).""" - user = models.OneToOneField(User, db_index=True, related_name="tradingaccount") - created = models.DateTimeField(db_index=True, auto_now_add=True) - balance = models.FloatField(default=0) # amount of money in the account - - def __unicode__(self): - return unicode(self.user) - - @staticmethod - def get(user, if_exists=False): - if not if_exists: - acct, isnew = TradingAccount.objects.get_or_create( - user = user, - defaults = { "balance": settings.PREDICTIONMARKET_SEED_MONEY } - ) - return acct - else: - try: - return TradingAccount.objects.get(user = user) - except TradingAccount.DoesNotExist: - return None - - def positions(self, **filters): - """Returns a dict from Outcomes to a dict containing shares held of each outcome, - the principle invested, and current unrealized profit/loss (i.e. if sold now).""" - def tuple_add(a, b): - return (a[0]+b[0], a[1]+b[1]) - p = { } - for trade in Trade.objects.filter(account=self, **filters).select_related("outcome", "outcome__market").order_by('created'): - p[trade.outcome] = tuple_add(p.get(trade.outcome, (0, 0)), (trade.shares, -trade.value)) - if p[trade.outcome][0] == 0: del p[trade.outcome] # clear when we hit zero so we don't carry forward *realized* profits/losses - p2 = { } - for outcome in p: - if p[outcome][0] != 0: - p2[outcome] = { - "shares": p[outcome][0], - "principle": -p[outcome][1], - "profitloss": -(outcome.market.transaction_cost({ outcome: -p[outcome][0] }) + p[outcome][1]) - } - return p2 - - def position_in_market(self, market): - """Returns a tuple of total number of shares held by this account in an outcome, - the amount invested, and the current unrealized profit/loss (i.e. if sold now).""" - positions = self.positions(outcome__market=market) - pl = 0.0 - for holding in positions.values(): - pl += holding["profitloss"] - return positions, pl - - def unrealized_profit_loss(self): - total = 0.0 - for p in self.positions().values(): - total += p["profitloss"] - return total - -class Market(models.Model): - """A prediction market comprising two or more outcomes.""" - - owner_content_type = models.ForeignKey(ContentType) - owner_object_id = models.PositiveIntegerField() - owner_object = generic.GenericForeignKey('owner_content_type', 'owner_object_id') - owner_key = models.CharField(max_length=16) - - name = models.CharField(max_length=128) - created = models.DateTimeField(db_index=True, auto_now_add=True) - volatility = models.FloatField(default=5.0) # prediction market volatility factor - volume = models.IntegerField(default=0) # total held shares across all outcomes - tradecount = models.IntegerField(default=0, db_index=True) # total number of trades across all outcomes - - isopen = models.BooleanField(default=True) - - def __unicode__(self): - return self.name - - def prices(self): - """Returns a dict mapping Outcome instances to floats in the range of 0.0 to 1.0 - indicating the current ('instantaneous') market price of the outcome.""" - - # Instantaneous prices are based on the same method used by Inkling - # Markets, i.e. Hanson's Market Maker, according to a blog post by David - # Pennock at http://blog.oddhead.com/2006/10/30/implementing-hansons-market-maker/. - # The price for any outcome i is: - # - # exp(q_i/b) - # ------------------------- - # exp(q_1/b) + exp(q_2/b) + ... - # - # where q_i is the number of shares outstanding for each outcome and b is the - # market volatility. - - prices = dict( (outcome, None) for outcome in self.outcomes.all() ) - denominator = 0.0 - for outcome in prices: - v = exp(outcome.volume / self.volatility) - denominator += v - prices[outcome] = v - for outcome in prices: - prices[outcome] /= denominator - return prices - - def cost_function(self, shares=None, outcomes=None): - """Returns the cost function where shares maps Outcome instances to the number of - outstanding shares for each outcome, or if None then the current outstanding shares - of each outcome.""" - - # The cost function, based on the methodology above, is: - # - # b * ln( exp(q_1/b) + exp(q_2/b) + ... ) - # - # where q_i is the number of shares outstanding for each outcome and b is the - # market volatility. - - - c = 0.0 - if not outcomes: outcomes = list(self.outcomes.all()) # let the caller cache the objects - for outcome in outcomes: - if shares: - v = shares.get(outcome, 0) - else: - v = outcome.volume - c += exp(v / self.volatility) - return self.volatility * log(c) - - def transaction_cost(self, shares, outcomes=None): - """Returns the cost to buy or sell shares of outcomes, where shares is a dict from - Outcome instances to the number of shares to buy (positive) or sell (negative).""" - - # The transaction cost is the cost function after the trade minus the cost function - # before the trade. - - if not outcomes: outcomes = list(self.outcomes.all()) # let the caller cache the objects - current_shares = dict((outcome, outcome.volume) for outcome in outcomes) - next_shares = dict((outcome, outcome.volume+shares.get(outcome, 0)) for outcome in outcomes) - return self.cost_function(next_shares, outcomes) - self.cost_function(current_shares, outcomes) - - def close_market(self): - # Any fields that are used during a trade should be modified synchronously. - cursor = connection.cursor() - cursor.execute("LOCK TABLES %s WRITE" % Market._meta.db_table) - try: - self.isopen = False - self.save() - finally: - cursor.execute("UNLOCK TABLES") - -class Outcome(models.Model): - market = models.ForeignKey(Market, related_name="outcomes") - owner_key = models.CharField(max_length=16) - name = models.CharField(max_length=128) - created = models.DateTimeField(db_index=True, auto_now_add=True) - volume = models.IntegerField(default=0) # total held shares - tradecount = models.IntegerField(default=0, db_index=True) # total number of trades - - def __unicode__(self): - return self.name - - def price(self): - return self.market.prices()[self] - - def liquidate(self, price): - """Forces the sale of all shares at a fixed price. Does not update market or - outcome volumes or tradecounts, since they are no longer relevant.""" - - # This method forces everyone to sell remaining shares at a price we set. - # The difficultly with this function is that the only way to know how many - # shares to sell for an account is to compute the account's position, which - # requires summing across all trades. - - cursor = connection.cursor() - cursor.execute("LOCK TABLES %s WRITE, %s WRITE, %s WRITE, %s WRITE" % (Trade._meta.db_table, Outcome._meta.db_table, Market._meta.db_table, TradingAccount._meta.db_table)) - try: - if self.market.isopen: raise ValueError("Market is open!") - - # Compute the outstanding shares for each account. - account_positions = { } - for trade in self.trades.all(): - account_positions[trade.account.id] = account_positions.get(trade.account.id, 0) + trade.shares - - for account, shares in account_positions.items(): - if shares == 0: continue - - account = TradingAccount.objects.get(id=account) - - # Record the transaction. - trade = Trade() - trade.account = account - trade.outcome = self - trade.shares = -shares - trade.value = price*shares - trade.liquidation = True - trade.save() - - # Update the account balance. - account.balance += price*shares - account.save() - finally: - cursor.execute("UNLOCK TABLES") - -class Trade(models.Model): - account = models.ForeignKey(TradingAccount, related_name="trades") - outcome = models.ForeignKey(Outcome, related_name="trades") - created = models.DateTimeField(db_index=True, auto_now_add=True) - shares = models.IntegerField() # shares bought (positive) or sold (negative). - value = models.FloatField() # monetary value of the transaction, positive means a credit to the account (selling shares), negative means a debit (buying shares) - liquidation = models.BooleanField(default=False) # if true, this is due to the liquidation of a closing market - - def purchase_price(self): - """Returns the average purchase price per share.""" - return abs(self.value / self.shares) - - @staticmethod - def place(account, outcome, shares, check_balance=True): - """Places a trade on an a market buying (shares>0) or selling (shares<0) - shares of outcome. Returns the new Trade instance.""" - - shares = int(shares) - if shares == 0: raise ValueException("shares must not be zero") - - # Trades must be synchronized because each trade affects the price of - # future trades. We must lock every table we touch during the transaction. - cursor = connection.cursor() - cursor.execute("LOCK TABLES %s WRITE, %s WRITE, %s WRITE, %s WRITE" % (Trade._meta.db_table, Outcome._meta.db_table, Market._meta.db_table, TradingAccount._meta.db_table)) - try: - outcomes = list(outcome.market.outcomes.all()) - - # Refresh objects now that the lock is held. - account = TradingAccount.objects.get(id=account.id) - outcome = Outcome.objects.get(id=outcome.id) - market = Market.objects.get(id=outcome.market.id) - - if not market.isopen: raise ValueError("Market is closed.") - - # What will this cost? - value = outcome.market.transaction_cost({ outcome: shares }, outcomes=outcomes) - - if shares > 0: - # If a buy, check that the account has enough money for this. - if check_balance and account.balance < value: - raise ValueError("Account does not have sufficient funds: %f needed." % value) - else: - # If a sale, check that the account has enough shares for this. While owning a negative amount of - # shares doesn't hurt the cost function, it does make an outcome's volume difficult to - # interpret (0 might mean an equal amount of buying and selling) and makes a market's - # volume completely nonsensical. - pos = Trade.objects.filter(account=account, outcome=outcome).aggregate(shares=models.Sum("shares")) - if pos["shares"] == None or pos["shares"] < shares: - raise ValueError("Account does not have sufficient shares: %d needed." % shares) - - # Record the transaction. - trade = Trade() - trade.account = account - trade.outcome = outcome - trade.shares = shares - trade.value = -value - trade.liquidation = False - trade.save() - - # Update the account balance. - account.balance -= value - account.save() - - # Update the outcome and market volumes and total trades count. - outcome.volume += shares - outcome.tradecount += 1 - outcome.save() - - market.volume += shares - market.tradecount += 1 - market.save() - finally: - cursor.execute("UNLOCK TABLES") - - return trade - - diff --git a/predictionmarket/urls.py b/predictionmarket/urls.py deleted file mode 100644 index cdf7d7ef..00000000 --- a/predictionmarket/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- -from django.conf.urls import * - -urlpatterns = patterns('predictionmarket.views', - url(r'^account$', 'accountinfo'), - -) - - diff --git a/predictionmarket/views.py b/predictionmarket/views.py deleted file mode 100644 index be26219f..00000000 --- a/predictionmarket/views.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -from django.http import Http404, HttpResponseRedirect -from django.shortcuts import redirect, get_object_or_404, render_to_response -from django.template import RequestContext -from django.core.urlresolvers import reverse -from django.core.cache import cache -from django.contrib.auth.decorators import login_required -from django.contrib.auth.models import User - -from common.decorators import render_to - -from predictionmarket.models import TradingAccount - -@login_required -@render_to('predictionmarket/account.html') -def accountinfo(request): - user = request.user - if user.is_superuser and "user" in request.GET: user = get_object_or_404(User, username=request.GET["user"]) - account = TradingAccount.get(user) - return { - 'account': account, - 'trades': account.trades.order_by("-created").select_related(), - } - diff --git a/settings.py b/settings.py index cada057a..073a1a0f 100644 --- a/settings.py +++ b/settings.py @@ -131,9 +131,6 @@ 'events', 'smartsearch', 'bill', - 'poll_and_call', - 'predictionmarket', - 'whipturk', # for django-registration-pv 'emailverification', @@ -173,9 +170,6 @@ EMAIL_UPDATES_RETURN_PATH = "GovTrack.us Email Updates " BOUNCES_UID_REGEX = re.compile(r"?", re.I) -PREDICTIONMARKET_SEED_MONEY = 1000 -PREDICTIONMARKET_BANK_UID = 136196 - #if DEBUG: # sometimes we debug in a live environment # EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/settings_env.py b/settings_env.py index ca922f1d..63ddd1f7 100644 --- a/settings_env.py +++ b/settings_env.py @@ -73,8 +73,6 @@ def get_env_boolvar(var_name, default=NOT_SET): FACEBOOK_APP_ID = get_env_variable('FACEBOOK_APP_ID', default='') FACEBOOK_APP_SECRET = get_env_variable('FACEBOOK_APP_SECRET', default='') FACEBOOK_AUTH_SCOPE = get_env_variable('FACEBOOK_AUTH_SCOPE', default='') # can be an empty string -TWILIO_ACCOUNT_SID = get_env_variable('TWILIO_ACCOUNT_SID', default='') -TWILIO_AUTH_TOKEN = get_env_variable('TWILIO_AUTH_TOKEN', default='') # TODO. The ad-free payment requires something like this: #import paypalrestsdk diff --git a/templates/bill/bill_details.html b/templates/bill/bill_details.html index 92fa3296..a5d6cfd2 100644 --- a/templates/bill/bill_details.html +++ b/templates/bill/bill_details.html @@ -54,14 +54,6 @@ // dynamic DOM elements $('#event_subscribe_inline_container').html(the_page.events_subscribe_button); if (the_page.admin_panel) $('#admin_panel').html(the_page.admin_panel); - if (the_page.poll_and_call_position) { - var p = the_page.poll_and_call_position; - $('#call_congress_positions').hide(); - $('#call_congress_position_chosen').show(); - $('#call_congress_position_chosen span').text(p.text); - $('#change_position').toggle(p.can_change); - $('#make_call').toggle(p.can_call.length > 0); - } // UI fixups $('#dead-prev-congress-popover').popover({ placement: 'bottom', trigger: 'hover' }) @@ -133,22 +125,6 @@ font-weight: bold; } -#call_congress_positions button { - display: inline-block; - margin-top: .75em; - margin-left: 2em; - padding: 8px; - font: 18px/24px Ubuntu, Verdana, Tahoma, Arial, sans-serif; - text-align: center; -} - #call_congress_positions button span { - display: block; - font-size: 42px; - margin-top: 10px; - } - #call_congress_positions button.support span { color: green; } - #call_congress_positions button.oppose span { color: red; } - #status-row-grid { } #status-row-grid tr { diff --git a/urls.py b/urls.py index 0f16cd77..f1063ab0 100644 --- a/urls.py +++ b/urls.py @@ -15,9 +15,6 @@ url(r'^congress/', include('vote.urls')), url(r'^congress/bills/', include('bill.urls')), url(r'', include('events.urls')), - url(r'^market/', include('predictionmarket.urls')), - url(r'^poll(/|$)', include('poll_and_call.urls')), - url(r'^publicwhip(/|$)', include('whipturk.urls')), url(r'^api/v2/([^/]+)(?:/(\d+))?', 'website.api.apiv2'), url(r'^_twostream', include('twostream.urls')), diff --git a/whipturk/__init__.py b/whipturk/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/whipturk/admin.py b/whipturk/admin.py deleted file mode 100644 index 466ab994..00000000 --- a/whipturk/admin.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 - -from django.contrib import admin -from whipturk.models import WhipReport - -class WhipReportAdmin(admin.ModelAdmin): - raw_id_fields = ('bill', 'target', 'user') - -admin.site.register(WhipReport, WhipReportAdmin) diff --git a/whipturk/models.py b/whipturk/models.py deleted file mode 100644 index 7663b345..00000000 --- a/whipturk/models.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- -from django.db import models -from jsonfield import JSONField -from common import enum - -import random - -MAX_CALLS_TO_TARGET = 50 -MAX_CALLS_FROM_USER = 50 -MAX_CALLS_TO_TARGET_ON_BILL = 3 -MAX_CALLS_TO_TARGET_FROM_USER = 3 - -class WhipReportType(enum.Enum): - self_report = enum.Item(1, 'Self Report') - cited_source = enum.Item(2, 'Cited Source') - phone_call = enum.Item(3, 'Phone Call') - -class WhipReportResult(enum.Enum): - invalid = enum.Item(0, 'Invalid') - not_entered = enum.Item(1, 'Not Entered') - unknown = enum.Item(2, 'No Information Found') - no_position = enum.Item(3, 'Member Has No Position') - supports = enum.Item(4, 'Member Supports The Bill') - opposes = enum.Item(5, 'Member Opposes The Bill') - its_complicated = enum.Item(6, 'It\'s Complicated') - got_voicemail = enum.Item(7, 'Got Voicemail') - asked_to_call_back = enum.Item(8, 'Asked To Call Back') - -class WhipReportReviewStatus(enum.Enum): - not_reviewed = enum.Item(0, 'Not Reviewed') - ok = enum.Item(1, 'OK') - bad = enum.Item(2, 'Bad') - -class WhipReport(models.Model): - """The result of calling Congress for information about a Member's position on a bill.""" - - user = models.ForeignKey('auth.User', db_index=True, help_text="The user making the phone call or reporting the information.", on_delete=models.PROTECT) - bill = models.ForeignKey('bill.Bill', db_index=True, help_text="The bill the call was about.", on_delete=models.PROTECT) - target = models.ForeignKey('person.PersonRole', db_index=True, help_text="The Member of Congress called.", on_delete=models.PROTECT) - - report_type = models.IntegerField(choices=WhipReportType, help_text="The nature of the report being made.") - report_result = models.IntegerField(choices=WhipReportResult, default=WhipReportResult.invalid, help_text="The information gleaned by this report.") - review_status = models.IntegerField(choices=WhipReportReviewStatus, default=WhipReportReviewStatus.not_reviewed, help_text="The information gleaned by this report.") - - citation_url = models.CharField(max_length=256, blank=True, null=True) - citation_title = models.CharField(max_length=256, blank=True, null=True) - citation_date = models.DateField(blank=True, null=True, help_text="The date on which the reported information was valid, if different from the creation date of this report.") - - created = models.DateTimeField(auto_now_add=True, db_index=True, help_text="The date and time the report was filed.") - updated = models.DateTimeField(auto_now=True, help_text="The date and time the report was filed.") - - call_status = models.CharField(max_length=64, blank=True, null=True) # status of a Twilio call in progress - call_log = JSONField(blank=True, null=True, help_text="A dict of TwilML information for different parts of the call.") - - class Meta: - ordering = ('-created',) - - def __unicode__(self): - return "%s / %s / %s / %s" % (self.created, self.user, self.bill, self.target.person) - - def has_made_successful_call(self): - return isinstance(self.call_log, dict) and self.call_log.get("finished", {}).get("RecordingUrl") is not None - - def get_result_description(self): - return WhipReport.get_result_nice_text(self.report_result, self.bill, self.target) - - def get_result_options(self): - return sorted( - (kv[0], WhipReport.get_result_nice_text(kv[0], self.bill, self.target)) - for kv in list(WhipReportResult) - if kv[0] != WhipReportResult.invalid - ) - - @staticmethod - def get_result_nice_text(report_result, bill, target): - if report_result == WhipReportResult.invalid: - return "The call was not completed." - if report_result == WhipReportResult.not_entered: - return "The result of the call has not yet been entered." - if report_result == WhipReportResult.unknown: - return "No useful information was gathered by this call." - if report_result == WhipReportResult.no_position: - return "%s does not have a position on %s." % (target.person.name_no_details(), bill.display_number) - if report_result == WhipReportResult.supports: - return "%s supports %s." % (target.person.name_no_details(), bill.display_number) - if report_result == WhipReportResult.opposes: - return "%s opposes %s." % (target.person.name_no_details(), bill.display_number) - if report_result == WhipReportResult.its_complicated: - return "%s does not have a simple position on %s." % (target.person.name_no_details(), bill.display_number) - if report_result == WhipReportResult.got_voicemail: - return "You got voicemail." - if report_result == WhipReportResult.asked_to_call_back: - return "You were asked to call back at another time." - - def should_set_result(self): - return self.report_result == WhipReportResult.not_entered - - def can_set_result(self): - return self.report_result != WhipReportResult.invalid diff --git a/whipturk/urls.py b/whipturk/urls.py deleted file mode 100644 index b64afacb..00000000 --- a/whipturk/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -from django.conf.urls import * - -urlpatterns = patterns('whipturk.views', - url(r'^my-calls$', 'my_calls'), - url(r'^start-call$', 'start_call'), - url(r'^_ajax/start-call$', 'dial'), - url(r'^_ajax/call-status$', 'call_status'), - url(r'^_ajax/update-report$', 'update_report'), - url(r'^_twilio/call-(?Pstart|input|transfer-end|end)/(?P\d+)$', 'twilio_callback'), -) diff --git a/whipturk/views.py b/whipturk/views.py deleted file mode 100644 index dd1b10f4..00000000 --- a/whipturk/views.py +++ /dev/null @@ -1,293 +0,0 @@ -# -*- coding: utf-8 -*- -from django.http import Http404, HttpResponseRedirect, HttpResponse -from django.shortcuts import redirect, get_object_or_404 -from django.contrib.auth.decorators import login_required -from django.conf import settings - -from common.decorators import render_to - -from datetime import datetime - -from whipturk.models import * - -from bill.models import Bill, Cosponsor -from person.models import PersonRole, RoleType -from person.analysis import load_sponsorship_analysis2 - -from twostream.decorators import anonymous_view, user_view_for -from registration.helpers import json_response - -targets_by_chamber = { - -} - -def get_targets_by_chamber(chamber): - if chamber not in targets_by_chamber: - # Get all current representatives or all senators. - role_type = RoleType.representative if chamber == "house" else RoleType.senator - targets = PersonRole.objects.filter( - current=True, - role_type=role_type)\ - .exclude(phone=None)\ - .select_related('person') - - # Create a priority score for each. We prefer to first call - # legislators in the middle of the ideology spectrum because - # they are least predictable, and also we prefer to call leaders - # because they are more important. - analysis = load_sponsorship_analysis2(settings.CURRENT_CONGRESS, role_type, None) - target_priorities = { } - - all_ideology_scores = [float(p["ideology"]) for p in analysis["all"]] - mean_ideology = sum(all_ideology_scores) / float(len(analysis["all"])) - absmax_ideology = max(abs(max(all_ideology_scores)-mean_ideology), abs(min(all_ideology_scores)-mean_ideology)) - - for p in analysis["all"]: - score = float(p["leadership"]) - abs(float(p["ideology"]) - mean_ideology)/absmax_ideology - target_priorities[p["id"]] = score - - targets_by_chamber[chamber] = [ - (target, target_priorities.get(target.person.id, 0.0)) - for target in targets - ] - - return targets_by_chamber[chamber] - -def choose_bill(): - # What shall we ask the user to call about? - bill_id = 286265 - return Bill.objects.get(id=bill_id) - -def choose_target(bill, user): - # Who could we ask the user to call? - chamber = bill.current_chamber - if chamber == None: - # It's not pending any action. This is bad. - return None - targets = get_targets_by_chamber(chamber) - target_scores = dict(targets) - - # Remove the sponsor and cosponsors --- we know their positions. - if bill.sponsor_role in target_scores: - del target_scores[bill.sponsor_role] - for cosp in Cosponsor.objects.filter(bill=bill): - if cosp.role in target_scores: - del target_scores[cosp.role] - - # Adjust scores by whether the target has been called before, and - # especially if this user made that call. - for wr in WhipReport.objects.filter(bill=bill): - if wr.target in target_scores: - target_scores[wr.target] -= 1.0 - if wr.user == user: - target_scores[wr.target] -= 1.0 - - # Bump score if this is the user's rep. - cd = user.userprofile().congressionaldistrict - for target in target_scores: - if cd and target.state == cd[0:2] and (target.role_type == RoleType.senator or int(cd[2:]) == target.district): - target_scores[target] += 1.0 - - # Take target with maximal score. - return max(target_scores, key = lambda t : target_scores[t]) - -@login_required -@render_to('whipturk/my_calls.html') -def my_calls(request): - return { - "calls": WhipReport.objects.filter(user=request.user), - } - -@login_required -@render_to('whipturk/place_call.html') -def start_call(request): - bill = choose_bill() - target = choose_target(bill, request.user) - if bill is None or target is None: - # Can't make a call now. Errr.... - return redirect('/publicwhip/my-calls') - - return { - "bill": bill, - "target": target, - "goodtime": (0 <= datetime.now().weekday() <= 4) and (9 <= datetime.now().time().hour <= 16), - } - -@login_required -@json_response -def dial(request): - bill = get_object_or_404(Bill, id=request.POST["bill"]) - target = get_object_or_404(PersonRole, id=request.POST["target"]) - - # basic abuse prevention - if WhipReport.objects.filter(bill=bill, target=target).count() > MAX_CALLS_TO_TARGET_ON_BILL: - return { "ok": False, "msg": "We've already made enough calls like that." } - if WhipReport.objects.filter(user=request.user, target=target).count() > MAX_CALLS_TO_TARGET_FROM_USER: - return { "ok": False, "msg": "You've already made enough calls like that." } - if WhipReport.objects.filter(user=request.user).count() > MAX_CALLS_FROM_USER: - return { "ok": False, "msg": "You've already made enough calls like that." } - if WhipReport.objects.filter(target=target).count() > MAX_CALLS_TO_TARGET: - return { "ok": False, "msg": "We've already made enough calls like that." } - - # basic validation of user's phone number - phone_num = "".join(c for c in request.POST["phone_number"] if unicode.isdigit(c)).encode("ascii") - if len(phone_num) != 10: - return { "ok": False, "msg": "Enter your area code and phone number." } - - # initialize twilio client - try: - from twilio.rest import TwilioRestClient - client = TwilioRestClient(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN) - except Exception as e: - return { "ok": False, "msg": "Site error: " + str(e) } - - # start a new report - report = WhipReport.objects.create( - user=request.user, - bill=bill, - target=target, - report_type=WhipReportType.phone_call, - call_status="not-yet-started", - call_log={}) - - call = client.calls.create( - to=("+1" + phone_num), - from_=settings.TWILIO_OUR_NUMBER, - url=build_twilio_callback_url(request, report, "call-start"), - status_callback=build_twilio_callback_url(request, report, "call-end"), - ) - - report.call_log["sid"] = call.sid - report.call_status = "started" - report.save() - - return { "ok": True, "call_id": report.id } - -def build_twilio_callback_url(request, report, method): - return request.build_absolute_uri("/publicwhip/_twilio/%s/%d" % (method, report.id)) - -@login_required -@json_response -def call_status(request): - report = get_object_or_404(WhipReport, id=request.POST["id"], user=request.user) - - if report.call_status == "not-yet-started": - msg = "Something went wrong, sorry!" - elif report.call_status == "started": - msg = "We are dialing your number..." - elif report.call_status == "picked-up": - msg = "You picked up. Dial 1 to be connected." - elif report.call_status == "connecting": - msg = "Connecting you to Congress..." - elif report.call_status == "connection-ended": - msg = "Ending the call... Hang on..." - elif report.call_status == "ended": - msg = "Call ended. Hang on..." - - return { "finished": report.call_status == "ended", "msg": msg } - -from twilio.twiml import Response as TwilioResponse -from django_twilio.decorators import twilio_view - -def get_request_log_info(request): - log_meta_fields = ('REMOTE_ADDR', 'HTTP_COOKIE') - ret = { f: request.META.get(f) for f in log_meta_fields } - ret["time"] = datetime.now().isoformat() - return ret - -@twilio_view -def twilio_callback(request, method, call_id): - # Dynamically invoke one of the methods below. - import whipturk.views - return getattr(whipturk.views, "twilio_call_" + method.replace("-", "_"))(request, call_id) - -def twilio_call_start(request, call_id): - report = get_object_or_404(WhipReport, id=int(call_id)) - report.call_status = "picked-up" - report.call_log["start"] = dict(request.POST) - report.call_log["start"]["_request"] = get_request_log_info(request) - report.save() - - resp = TwilioResponse() - resp.say("Hello from Gov Track.") - g = resp.gather( - action=build_twilio_callback_url(request, report, "call-input"), - numDigits=1, - timeout=20, - ) - g.say("Press one to be connected to the office of %s %s. Press two if you did not request this call. Or simply hang up if you do not want your call to be connected." % ( - report.target.get_title(), - report.target.person.lastname)) - resp.say("Oooo too slow. We're going to hang up now.") - - return resp - -def twilio_call_input(request, call_id): - report = get_object_or_404(WhipReport, id=int(call_id)) - digit = request.POST["Digits"] - - report.call_log["input"] = dict(request.POST) - report.call_log["input"]["_request"] = get_request_log_info(request) - - resp = TwilioResponse() - - if digit != "1": - # basically an abuse report - report.call_log["input"]["response"] = "did-not-request-call" - resp.say("We apologize for the inconvenience. Call 202-558-7227 or visit w w w dot gov track dot u s to report abuse. Good bye.") - resp.hangup() - - elif settings.DEBUG: - resp.say("Site is in debug mode. Call cancelled.") - resp.hangup() - - else: - phone = "+1" + "".join(c for c in report.target.phone if unicode.isdigit(c)) - - report.call_log["input"]["response"] = "continue" - report.call_log["input"]["transfer_to"] = phone - - resp.say("Okay. Hold on.") - resp.dial( - phone, - action=build_twilio_callback_url(request, report, "call-transfer-end"), - timeout=30, - callerId=request.POST["To"], - record=True, - ) - - report.call_status = "connecting" - report.save() - - return resp - -def twilio_call_transfer_end(request, call_id): - report = get_object_or_404(WhipReport, id=int(call_id)) - report.call_status = "connection-ended" - report.call_log["finished"] = dict(request.POST) - report.call_log["finished"]["_request"] = get_request_log_info(request) - report.save() - - resp = TwilioResponse() - resp.say("Your call to Congress has ended. Thank you for being a great citizen. Goodbye.") - return resp - -def twilio_call_end(request, call_id): - report = get_object_or_404(WhipReport, id=int(call_id)) - report.call_status = "ended" - report.call_log["end"] = dict(request.POST) - report.call_log["end"]["_request"] = get_request_log_info(request) - report.report_result = WhipReportResult.not_entered - report.save() - - # empty response - resp = TwilioResponse() - return resp - -@login_required -@json_response -def update_report(request): - report = get_object_or_404(WhipReport, id=request.POST['report'], user=request.user) - report.report_result = int(request.POST['value']) - report.save() - return { "status": "ok" }