Giter VIP home page Giter VIP logo

hsvdotbeer's Introduction

hsvdotbeer

Built with

Collate ALL the Huntsville beers. Check out the project's documentation.

What is this?

  • It's a concept spearheaded by @mjcarroll, @drewbrew, and @drewmcdowell to make a one-stop-shop for all the Huntsville-area breweries, taprooms, and bottle shops.

I want my venue listed!

First, three questions:

  1. Are you located within the city of Huntsville, the city of Madison, or Madison County, Alabama (this includes smaller cities like Triana or Owens Cross Roads)?
  2. Do you have at least 6 beer taps in regular use?
  3. Of those taps, are 50% or 5 taps, whichever is lower, typically featuring beers made in Alabama or Middle Tennessee (we're pretty flexible here)?

If the answer to all 3 of those is yes, great! If not, ask us anyway and we may be willing to grant you an exception. We're doing this in our spare time and don't have time to get every bar in Alabama signed up!

Sign up for a free account at https://taplist.io/. Once you're in, create your taps and beers.

At that point, contact us either via Twitter or creating an issue here. We'll guide you through the process of creating a fake display that we'll use to get the info we need. It doesn't matter whether you want to use a chalkboard or a digital display; if you want to do the latter, you can simply create another display.

If you want to have a digital display, you can use either a Fire TV Stick or a Raspberry Pi. They have great instructions for both at their help site.

How do I get started?

Reading material

  • First, get yourself familiar with Django. There are two excellent tutorials to get yourself started:
    • Django Girls assumes you have no experience with Python or the command line and is a great place for total newbies to get started.
    • The Django tutorial assumes a little bit of basic Python knowledge but is also good.
  • Next, take a look at the Django REST Framework tutorial

Installing software

  • Python 3.7
    • Windows 10 users can also install Python from the Windows Store
  • Docker (Download for your platform)
    • NOTE: if you intend to develop on Windows, you need to have Windows 10 Pro or Enterprise to be able to use Docker, and you have to have at least a somewhat recent CPU that supports Hyper-V. Any non-Atom CPU from the past 5 years should more than suffice. Also, it'll break VirtualBox 5.x and older.

Setting Up a Dev Environment

  • After you get Python installed, you need to open a command line (see the Django Girls tutorial above) and run pip3 install pipenv to be able to install packages.
  • Once you have pipenv installed, install packages:
pipenv install

Local Development

Build the environment

Build the docker images:

docker-compose build

Run the app

Start the dev server for local use:

docker-compose up

That will automatically pull the required images, install packages, and launch the processes. If you need to rebuild your images (such as when dependencies change), you can add --build to the end of that command to re-fetch images and build.

Run a command inside the docker container:

docker-compose run --rm web [command]

To get to a plain shell, run docker-compose run --rm web bash. From there, you can run Django commands like pipenv run ./manage.py shell.

Running without Docker

Say you don't want to use Docker. Don't worry, here's what you need to get started:

pipenv install --dev
export DJANGO_SECRET_KEY=your_secret_key
pipenv run python manage.py runserver

You'll need to set this up anyway if you're making migrations (i.e. modifying models) outside the docker shell.

Contributing and Community

PRs are more than welcome. As we get a better idea of what we need to do, we'll create issues that need fixing if you don't know where to start. All contributors are required to follow the Tech256 Code of Conduct.

You can find us on the Tech256 Slack in the #hsv_dot_beer channel.

hsvdotbeer's People

Contributors

austincgomez avatar danroberts728 avatar dependabot[bot] avatar drewbrew avatar mjcarroll avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

hsvdotbeer's Issues

Add pricing

  1. Create a ServingSize model within the beer app:
    • volume_oz (nullable unique decimal field)
    • name (unique)
    • Constraints:
  2. Create a BeerPrice model:
    • beer (FK to Beer)
    • size (FK to ServingSize)
    • venue (FK to Venue)
    • price (Decimal)
    • Constraint: beer, venue, and size are unique together
  3. When merging beers, copy over pricing
  4. Expose pricing info in beer serializer
  5. Update parsers to save pricing if available in API (and create sizes if not known)
  6. Write fixture for common sizes
    • 4 oz taster
    • half pint
    • 10 oz
    • 14.5 oz
    • pint
    • 32 oz crowler/half growler
    • 64 oz growler

Write Parser for Stem & Stein

This one is going to be a bit of a challenge:

  • On the main page, there's a <div class="beerlist">
  • Find all the <a>s in there, and follow each link.
  • On each page:
<div class="jumbotron">
    <div class="container">
        <div style="display:table-row">
            <div style="display:table-cell;vertical-align:top;width:17px;"><img style="width:17px;padding-top:2px" src="/Display/GlassWare?color=%23261716&amp;glassware=Tulip" /></div>
            <div style="display: table-cell; padding: 3px; font-size: 22px; vertical-align: top; width:42px">$9 </div>
            <div style="display:table-cell">
                <div class="fill" style="color:white; font-size:30px"><span>Prairie Deconstructed Bomb! Vanilla Imperial Stout</span></div>
                <div style="color:slategray; font-size:18px;padding-left:20px">ABV 13% &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Tulsa, OK</div>
            </div>
        </div>
    </div>
</div>
  • Look for a <div style="display:table-row">.
  • Within there:
    • There'll be an img tag pointing to the size and color: /Display/GlassWare?color=%23261716&amp;glassware=Tulip (will have to un-urlencode to color=#261716&glassware=Tulip). Color is a straight HTML color. Glassware gives us our size: 10 oz or 16 oz.
    • There will be a <div> whose text starts with $. That's your price.
    • The other <div> with text is the brewery and beer on one line. That makes things hard.
    • The <span> gives us ABV and location

Cache untappd metadata

  1. Create an untappd_cache table
    • OneToOneField with Beer
    • last_updated
    • metadata (type JSONField)
  2. Create celery task to prune stale metadata entries (have it run at something like 5 AM CDT)
  3. Have the beer details view hit untappd if the beer is not in metadata
  4. When listing beers, asynchronously fetch metadata for the beer if not present (so it can show up for the next person)
  5. Embed untappd data in beer serializer

Make brewery and manufacturer names unique without regard to case

  1. Create a data migration to merge manufacturers whose names differ only in case
    from collections import Counter
    names = [i.name.casefold() for i in Manufacturer.objects.all()]
    dupes = [key for key, val in Counter(names) if val > 1]
    needing_merge = Manufacturer.objects.filter(name__iregex=f'({"|".join(dupes)})')
    # then copy the merge method and go through each one
  2. Create an empty migration to enable the PostgreSQL case-insensitive extension.
  3. Convert Manufacturer.name into a CITextField (PostgreSQL won't enforce length anyway)
  4. Repeat steps 1 and 3 for Beer

Add filtering on venues

Things I can think of:

  • name
  • beer name
  • beer style name
  • beer style category name

Does filtering by manufacturer make sense? I can't say I've ever wondered what beers by Straight to Ale are on tap at Wish You Were Beer.

Make docs less terrible

  1. import fixtures
  2. missing models
  3. parser design pattern (for writing new ones)
  4. moderation (when designed)
  5. Instructions for adding venues for the different parsers.

Create a moderation queue for new beers

If we get a beer coming in that we can't cleanly parse (likely because its style doesn't map to BJCP), use a moderation queue to make our lives way easier.

Also consider adding a StyleMapping model:

  • BeerStyle (FK)
  • provider (CharField, same choices as Venue.taplist_provider)
  • provider_style_name

That way we can auto-resolve future styles with the same name.

Implement Event app/model

Fields:

  • Venue (required)
  • Start time (required)
  • End time (required, set to end of day in venue time zone if specified, otherwise 1 hour after start)
  • Description

NOTE: In the future we may want to adopt a more robust calendar model to handle repeating events/all day events, but this is good for the basics.

Implement Tap model and app

Fields:

  • Room (required, depends on #6 -- merged)
  • Tap number (required)
  • Beer (optional, foreign key, depends on #16)
  • Gas type (choices: CO2, nitro, unknown/null)
  • Estimated percent remaining (float, values 0-100, nullable)
    • Some providers give an estimate of how much is left. Let's take advantage of that

Unique indexes:

  • Room + tap number

BeerAdvocate URL for Ace Pineapple points to RateBeer

From digitalpour data for WYWB805:

{
  "Id": "5c6dd6d235272612e83ae095",
  "MenuItemDisplayDetail": {
    "$type": "BeerDashboard.Common.Models.Tap, BeerDashboard.Common",
    "BeverageType": null,
    "TapType": "CO2",
    "NonRotating": false,
    "ParentTapId": null,
    "LineLength": null,
    "LineDiameter": null,
    "CO2Mix": null,
    "NitroMix": null,
    "PSI": null,
    "Id": "58882be35e002c105cb7e4bc",
    "CompanyId": "57b130dd5e002c0388f8b686",
    "CompanyName": "Wish You Were Beer",
    "LocationId": "2",
    "LocationName": "Campus 802",
    "ItemType": "Tap",
    "DisplayName": "37",
    "DisplayOrder": 37,
    "DisplayLogoUrl": null,
    "DisplayGroup": null,
    "CostCenter": null,
    "NotInUse": false,
    "StorageLocation": {
      "CompanyId": "57b130dd5e002c0388f8b686",
      "CompanyName": null,
      "LocationId": "2",
      "LocationName": "Campus 802",
      "IsUsable": true,
      "DefaultKegLocation": true,
      "TemperatureSensorId": null,
      "Temperature": 0.0,
      "Id": "58882a955e002c105cb7e4ad",
      "StorageLocationName": "Cooler"
    },
    "ProductId": "H9V0TYD4QY5PT",
    "MeasuringSystemMappingId": null,
    "EventTableId": null,
    "TemperatureSensorId": null,
    "Temperature": 0.0,
    "IsDirty": false
  },
  "MenuItemProductDetail": {
    "$type": "BeerDashboard.Common.Models.Keg, BeerDashboard.Common",
    "KegSize": 661.0,
    "KegType": "CO2",
    "Coupler": "US Sankey (D)",
    "BatchId": null,
    "DateKegged": null,
    "KegId": null,
    "InitialOuncesConsumed": 0.0,
    "SamplesPoured": 0,
    "SampleSize": 0.0,
    "OuncesConsumed": 46.5,
    "PercentFull": 0.92965204236006049,
    "UseMeasuredValues": false,
    "PosReportedOuncesConsumed": 46.5,
    "PosReportedPercentFull": 0.92965204236006049,
    "MeasuredOuncesConsumed": 0.0,
    "MeasuredPercentFull": 0.0,
    "DaysOn": 1,
    "TimeOn": 188.01611686833334,
    "AllowedTaps": null,
    "PercentConsumedBySamples": 0.0,
    "EstimatedOzLeft": 614.5,
    "HasRestrictions": false,
    "RestrictedReplacementsList": null,
    "EstimatedKegLeftDuration": "1.17:25:00",
    "AvailableInBottles": false,
    "MoreKegsAvailable": false,
    "Id": "2cb7119e-0ba8-479d-9af2-dcdb6a23bb39",
    "BeverageType": "Cider",
    "Beverage": {
      "$type": "BeerDashboard.Common.Models.CiderModels.Cider, BeerDashboard.Common",
      "BeverageName": "Pineapple",
      "Cidery": {
        "CideryName": "Ace",
        "CideryUrl": "http://www.acecider.com/",
        "CultureSpecificCideryNames": {},
        "CultureSpecificLocationNames": {},
        "ProducerName": "Ace",
        "SimplifiedProducerName": "ace",
        "CultureAwareCideryName": "Ace",
        "CultureAwareLocationName": "Sebastopol, CA",
        "Id": "51d75e05df752b2124c127d1",
        "FullProducerName": null,
        "Location": "Sebastopol, CA",
        "ProducersUrl": null,
        "LogoImageUrl": "https://s3.amazonaws.com/digitalpourproducerlogos/51d75e05df752b2124c127d1.png",
        "TwitterName": "@AceCider",
        "Latitude": 0.0,
        "Longitude": 0.0,
        "DefaultKegSize": 1984.0,
        "DefaultKegCoupler": "US Sankey (D)",
        "CompanyId": null,
        "IsAmateur": false,
        "AlternateLocations": []
      },
      "Collaborators": [],
      "CiderName": "Pineapple",
      "CiderStyle": {
        "Id": "54ab0cc8b3b6f60eccac28a0",
        "StyleName": "Pineapple Cider",
        "ParentId": null,
        "ParentIds": ["52a4d173fb890c0f541fc9c5"],
        "Color": 15921786,
        "RecommendedCO2Mix": null,
        "RecommendedNitroMix": null,
        "RecommendedPSI": null,
        "CultureSpecificStyleNames": {},
        "CultureAwareStyleName": "Pineapple Cider"
      },
      "StyleVariation": "",
      "StyleVariationPrefix": null,
      "Dryness": "Semi-Sweet",
      "BaseFruit": {
        "Apple": 1.0
      },
      "BarrelAging": null,
      "HopsUsed": null,
      "YeastUsed": "Champagne",
      "Abv": 5.0,
      "Ibu": 0.0,
      "OriginalGravity": null,
      "FinalGravity": null,
      "pH": null,
      "Attributes": null,
      "CiderUrl": "acecider.com/our-ciders/ace-pineapple/",
      "CultureSpecificCiderNames": {},
      "RateBeerUrl": null,
      "BeerAdvocateUrl": "http://www.ratebeer.com/beer/ace-pear-cider/3004/",
      "UntappdUrl": null,
      "CollaboratorList": "",
      "BeverageProducer": {
        "$type": "BeerDashboard.Common.Models.CiderModels.Cidery, BeerDashboard.Common",
        "CideryName": "Ace",
        "CideryUrl": "http://www.acecider.com/",
        "CultureSpecificCideryNames": {},
        "CultureSpecificLocationNames": {},
        "ProducerName": "Ace",
        "SimplifiedProducerName": "ace",
        "CultureAwareCideryName": "Ace",
        "CultureAwareLocationName": "Sebastopol, CA",
        "Id": "51d75e05df752b2124c127d1",
        "FullProducerName": null,
        "Location": "Sebastopol, CA",
        "ProducersUrl": null,
        "LogoImageUrl": "https://s3.amazonaws.com/digitalpourproducerlogos/51d75e05df752b2124c127d1.png",
        "TwitterName": "@AceCider",
        "Latitude": 0.0,
        "Longitude": 0.0,
        "DefaultKegSize": 1984.0,
        "DefaultKegCoupler": "US Sankey (D)",
        "CompanyId": null,
        "IsAmateur": false,
        "AlternateLocations": []
      },
      "BeverageStyle": {
        "$type": "BeerDashboard.Common.Models.CiderModels.CiderStyle, BeerDashboard.Common",
        "Id": "54ab0cc8b3b6f60eccac28a0",
        "StyleName": "Pineapple Cider",
        "ParentId": null,
        "ParentIds": ["52a4d173fb890c0f541fc9c5"],
        "Color": 15921786,
        "RecommendedCO2Mix": null,
        "RecommendedNitroMix": null,
        "RecommendedPSI": null,
        "CultureSpecificStyleNames": {},
        "CultureAwareStyleName": "Pineapple Cider"
      },
      "FullCideryList": "Ace",
      "ResolvedLogoImageUrl": "https://s3.amazonaws.com/digitalpourproducerlogos/51d75e05df752b2124c127d1.png",
      "FullStyleName": "Pineapple Cider ",
      "ExpandedStyleName": "Pineapple Cider ",
      "CultureAwareBeverageName": "Pineapple",
      "FullProducerList": "Ace",
      "StyleColor": 15921786,
      "Id": "52e05208fb890c0980933935",
      "LogoImageUrl": null,
      "CO2Content": null,
      "CaloriesPerOz": null,
      "CustomDescription": null,
      "CustomStyle": null
    },
    "DateProduced": "0001-01-01T00:00:00Z",
    "Year": 2019,
    "BeverageCategory": "Craft",
    "BeverageCategoryLogoUrl": null,
    "Attributes": [],
    "CustomBeverageIcon": null,
    "HasVintage": false,
    "Prices": [{
      "Id": "T",
      "Size": 5.0,
      "Price": 2.25,
      "DisplayName": "5oz",
      "DisplaySize": 5.0,
      "PosModifier": "AZZAQR9A21E0Y",
      "SizeInPos": null,
      "UPCCode": null,
      "CostCenter": null,
      "Glassware": "Taster",
      "DisplayOnMenu": true,
      "Deactivated": false
    }, {
      "Id": "A",
      "Size": 9.5,
      "Price": 4.5,
      "DisplayName": "10oz",
      "DisplaySize": 10.0,
      "PosModifier": "F56XWN3E24HKM",
      "SizeInPos": null,
      "UPCCode": null,
      "CostCenter": null,
      "Glassware": "Half Snifter",
      "DisplayOnMenu": false,
      "Deactivated": false
    }, {
      "Id": "B",
      "Size": 15.5,
      "Price": 7.0,
      "DisplayName": "16oz",
      "DisplaySize": 16.0,
      "PosModifier": "7ZQ9HTJTT9S6T",
      "SizeInPos": null,
      "UPCCode": null,
      "CostCenter": null,
      "Glassware": "Pint 2",
      "DisplayOnMenu": true,
      "Deactivated": false
    }, {
      "Id": "C",
      "Size": 35.2,
      "Price": 10.0,
      "DisplayName": "32oz",
      "DisplaySize": 32.0,
      "PosModifier": "7WPW4D5J60PAY",
      "SizeInPos": null,
      "UPCCode": null,
      "CostCenter": null,
      "Glassware": "Growler (Small)",
      "DisplayOnMenu": false,
      "Deactivated": false
    }, {
      "Id": "D",
      "Size": 70.4,
      "Price": 20.0,
      "DisplayName": "64oz",
      "DisplaySize": 64.0,
      "PosModifier": "E62P5QRBE47GW",
      "SizeInPos": null,
      "UPCCode": null,
      "CostCenter": null,
      "Glassware": "Growler",
      "DisplayOnMenu": true,
      "Deactivated": false
    }],
    "EventPrices": [{
      "Id": "T",
      "Size": 5.0,
      "Price": 2.25,
      "DisplayName": "5oz",
      "DisplaySize": 5.0,
      "PosModifier": "AZZAQR9A21E0Y",
      "SizeInPos": null,
      "UPCCode": null,
      "CostCenter": null,
      "Glassware": "Taster",
      "DisplayOnMenu": true,
      "Deactivated": false
    }, {
      "Id": "A",
      "Size": 9.5,
      "Price": 4.5,
      "DisplayName": "10oz",
      "DisplaySize": 10.0,
      "PosModifier": "F56XWN3E24HKM",
      "SizeInPos": null,
      "UPCCode": null,
      "CostCenter": null,
      "Glassware": "Half Snifter",
      "DisplayOnMenu": false,
      "Deactivated": false
    }, {
      "Id": "B",
      "Size": 15.5,
      "Price": 6.0,
      "DisplayName": "16oz",
      "DisplaySize": 16.0,
      "PosModifier": "7ZQ9HTJTT9S6T",
      "SizeInPos": null,
      "UPCCode": null,
      "CostCenter": null,
      "Glassware": "Pint 2",
      "DisplayOnMenu": true,
      "Deactivated": false
    }, {
      "Id": "C",
      "Size": 35.2,
      "Price": 8.5,
      "DisplayName": "32oz",
      "DisplaySize": 32.0,
      "PosModifier": "7WPW4D5J60PAY",
      "SizeInPos": null,
      "UPCCode": null,
      "CostCenter": null,
      "Glassware": "Growler (Small)",
      "DisplayOnMenu": false,
      "Deactivated": false
    }, {
      "Id": "D",
      "Size": 70.4,
      "Price": 17.0,
      "DisplayName": "64oz",
      "DisplaySize": 64.0,
      "PosModifier": "E62P5QRBE47GW",
      "SizeInPos": null,
      "UPCCode": null,
      "CostCenter": null,
      "Glassware": "Growler",
      "DisplayOnMenu": true,
      "Deactivated": false
    }],
    "EventPricesActive": false,
    "EventId": null,
    "EventName": null,
    "DateAdded": "2019-02-20T06:54:09.68Z",
    "DoNotUse": false,
    "ProductFinished": false,
    "ReplacesBeverageIds": [],
    "BeverageNameWithVintage": "Pineapple",
    "FullBeverageName": "Ace Pineapple",
    "FullProducerList": "Ace",
    "FullStyleName": "Pineapple Cider ",
    "OverrideableFullStyleName": "Pineapple Cider ",
    "EventItem": false,
    "ReplaceableItem": false
  },
  "Active": true,
  "EstimatedDatePutOn": null,
  "DatePutOn": "2019-02-20T22:38:12.893Z",
  "DatePulledOff": null,
  "QuantityOnTap": 0
}

Also, LOL@ Campus 802

Setup fuzzy search

The search field will search across multiple datasets: beer name, manufacturer name, and beer style. The logic for this will need to be created.

Bogus RateBeer and BeerAdvocate URLs from OTBX

https://dev.hsv.beer/api/v1/beers/788/

{
    "id": 788,
    "manufacturer": {
        "id": 284,
        "name": "Franziskaner",
        "url": "",
        "location": "Munich, Germany",
        "logo_url": "https://s3.amazonaws.com/digitalpourproducerlogos/525853c6fb890c252c69df77.png",
        "facebook_url": "",
        "twitter_handle": "",
        "instagram_handle": "",
        "untappd_url": null,
        "automatic_updates_blocked": false,
        "taphunter_url": null
    },
    "abv": "5.00",
    "color_srm": null,
    "color_srm_html": "#efef07",
    "style": null,
    "venues": [
        {
            "id": 4,
            "time_zone": "America/Chicago",
            "tap_list_provider_display": "DigitalPour",
            "name": "Old Town Beer Exchange",
            "address": "301 Holmes Avenue",
            "city": "Huntsville",
            "state": "Alabama",
            "postal_code": "35801",
            "country": "US",
            "website": "http://otbxhsv.com",
            "facebook_page": "https://www.facebook.com/OTBXHSV",
            "twitter_handle": "OTBXHSV",
            "instagram_handle": "otbxhsv",
            "tap_list_provider": "digitalpour",
            "untappd_url": null
        }
    ],
    "prices": [
        {
            "venue": "Old Town Beer Exchange",
            "serving_size": {
                "name": "Growler",
                "volume_oz": "64.0"
            },
            "price": "14.00"
        },
        {
            "venue": "Old Town Beer Exchange",
            "serving_size": {
                "name": "Crowler/Half Growler",
                "volume_oz": "32.0"
            },
            "price": "7.00"
        },
        {
            "venue": "Old Town Beer Exchange",
            "serving_size": {
                "name": "Pint",
                "volume_oz": "16.0"
            },
            "price": "5.50"
        },
        {
            "venue": "Old Town Beer Exchange",
            "serving_size": {
                "name": "10 oz",
                "volume_oz": "10.0"
            },
            "price": "4.50"
        },
        {
            "venue": "Old Town Beer Exchange",
            "serving_size": {
                "name": "4 oz Taster",
                "volume_oz": "4.0"
            },
            "price": "2.50"
        }
    ],
    "untappd_metadata": null,
    "name": "Hefe-Weisse",
    "in_production": true,
    "ibu": 4,
    "untappd_url": null,
    "beer_advocate_url": "http%3A%2F%2Fbeeradvocate.com%2Fbeer%2Fprofile%2F142%2F924",
    "rate_beer_url": null,
    "logo_url": "https://s3.amazonaws.com/digitalpourbeveragelogos/50941bb49294c325343962b6.png",
    "manufacturer_url": null,
    "automatic_updates_blocked": false,
    "taphunter_url": null,
    "stem_and_stein_pk": null
}

Note that the BA and RB URLs are URL-quoted. This came straight from the OTBX JSON data.

Fix Stem and Stein brewery guessing logic

  • Picked the wrong stone brewing company (solution: try istartswith before icontains)
    image
  • Picked Goodwood instead of Good People (solution: work from longest name first rather than shortest)
    image
  • Picked Horny Goat Brewing instead of Goat Island (same solution as previous bullet)
    image

Add route: /venues/<pk>/styles

List styles available at a venue with beers expanded from that, e.g.

{
  "style": {
    <style serializer>,
    "beers": [<list of beer serializers>]
  }, ...
}

Open up auth on views

  • Almost all views (except users) should be read-only to anybody who isn't authorized.
  • Most views should be writable by staff
  • The BeerStyle viewset should be writable by nobody

Implement scraper for The Nook

As far as we can tell, The Nook uses a static HTML page to host its beer list. Create a scraper (hello, beautifulsoup) that parses the HTML data, tries to make a guess based on style/name/ABV, and saves the data to taps for the Nook

Drop asterisks in Nook beers

The Nook uses an asterisk on some beers to denote... something (super expensive beers?).

We need to strip those out.

Jump from Travis to... something

Since Idera fired all the Travis devs and we need to leave travis-ci.org anyway, look at other options and use one of those.

Ideas:

  • CircleCI
  • Azure devops

I need this to fix #4.

Expand manufacturer lookup to handle common endings

When looking up manufacturers, ignore common endings:

  1. Brewing Company
  2. Brewery
  3. Brewing
  4. Brewing Co.
  5. Brewing
  6. Beer Company
  7. Beer
  8. Beer Co.

In other words, if a manufacturer given from the API provider ends in one of those, strip it off before searching for the manufacturer.

Also create a data migration to auto-merge breweries with those endings and standardize on the ending-less version for consistency (e.g. Stone over Stone Brewing Company)

Implement Beer model in the beers app

Fields:

  • Name

  • Manufacturer (foreign key; depends on #15 -- merged)

  • Style (foreign key; depends on #10 -- merged)

  • In production (boolean, default True)

  • ABV (optional)

  • SRM (optional)

  • IBU (optional)

  • Untappd ID (optional, indexed)

  • BeerAdvocate URL (optional, indexed)

  • RateBeer URL (optional, indexed)

  • Unique index: manufacturer + name

Remaining venues

Venues we should theoretically be able to support

  • The Bar at 805 (DigitalPour, but no site that I know of)
  • Pints and Pixels (Untappd, but doesn't expose the tap list on their site) (2019-03-28: @mjcarroll emailed them about it)
  • Below the Radar (Untappd, but doesn't have a functioning site to expose the tap list on)

Venues we'll never support

  • Green Bus
  • Salty Nut
  • OBB
  • Church Street Wine Shoppe

Places to consider but may not fit in with our target audience

Untappd

  • Cajun Steamer (Twickenham, no beer menu on site)
  • Anaheim Chili (they have a beer menu on their site, but it's down they let their domain get hijacked and their email address bounced)
  • End Zone (no beer menu on their site)
  • Stand Up Live (no beer menu on their site)
  • West End Outdoors (Athens, no website)

Weekly notification for new beers

As a moderation helper, send a periodic email (weekly?) listing all new beers added over the past week so we can moderate them if needed.

Take the most naive approach possible to start:

  • Create a model for logging highest beer PK seen
  • Set up a task that looks for all beers higher than that PK and email that list, then update the highest PK

Drop rooms

The only venue where room matters doesn't expose the room via API, so kill it with fire.

Implement Venue app & model

Fields:

  • Name (unique, required)
  • Address
  • City
  • State/Province
  • Postal Code
  • Country
  • URL
  • Facebook link
  • Twitter handle
  • Instagram handle
  • Time Zone (default configurable in settings)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.