hsv-dot-beer / hsvdotbeer Goto Github PK
View Code? Open in Web Editor NEWCollate ALL the beers!
License: Apache License 2.0
Collate ALL the beers!
License: Apache License 2.0
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
Create a tap_list_providers
app and implement retrieval and parsing of data for a brewery via DigitalPour's API.
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
List beers on tap by style, with venue embedded therein.
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.
Fields:
The only venue where room matters doesn't expose the room via API, so kill it with fire.
http://docs.celeryproject.org/en/latest/django/first-steps-with-django.html
pipenv install celery
The Nook uses an asterisk on some beers to denote... something (super expensive beers?).
We need to strip those out.
For innerspace. I have a prototype, just need to port it into the parser format.
Fields:
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.
Title says it all
ServingSize
model within the beer app:
volume_oz
(nullable unique decimal field)name
(unique)BeerPrice
model:
beer
(FK to Beer
)size
(FK to ServingSize
)venue
(FK to Venue
)price
(Decimal)Fields:
Unique index:
Title says it all
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
Copy the info from #28 into the BeerStyle model.
Things I can think of:
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.
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:
I need this to fix #4.
Things I can think of:
This answers the user question Where can I find monkeynaut?
List styles available at a venue with beers expanded from that, e.g.
{
"style": {
<style serializer>,
"beers": [<list of beer serializers>]
}, ...
}
Title says it all
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
CITextField
(PostgreSQL won't enforce length anyway)Same as #74, only from style categories
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.
When looking up manufacturers, ignore common endings:
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
)
The search field will search across multiple datasets: beer name, manufacturer name, and beer style. The logic for this will need to be created.
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:
Fields:
Unique indexes:
Same as #70, only from a categories view.
This happens if one entry for a manufacturer has a different value for location than another.
This one is going to be a bit of a challenge:
<div class="beerlist">
<a>
s in there, and follow each link.<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&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% Tulsa, OK</div>
</div>
</div>
</div>
</div>
<div style="display:table-row">
.img
tag pointing to the size and color: /Display/GlassWare?color=%23261716&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.<div>
whose text starts with $
. That's your price.<div>
with text is the brewery and beer on one line. That makes things hard.<span>
gives us ABV and locationOr switch to saving the HTML color...
OneToOneField
with Beer
last_updated
metadata
(type JSONField
)Fields:
Implement access to Untappd data
List beers available at a venue (shorthand for filtering by venue)
They seem neat.
Fall back to ResolvedLogoImageUrl
if LogoImageUrl
is null in digitalpour parsing.
https://hsv-dot-beer.github.io/hsvdotbeer/ is missing images
This is hopefully an easy fix
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.