├── sv ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── initialize_latests.py │ │ ├── scraper.py │ │ ├── new_submissions.py │ │ ├── searcher.py │ │ ├── new_comments.py │ │ └── cmd_helper.py ├── templatetags │ ├── __init__.py │ └── sv_extras.py ├── helpers.py ├── forms.py ├── views_rest.py ├── serializers.py ├── templates │ ├── esv_tabler.html │ ├── sample_body.html │ ├── base.html │ ├── dump_form.html │ └── result.html ├── admin.py ├── natures.py ├── views.py ├── iv.py ├── models.py ├── dump_search.py ├── managers.py └── iv_tests.py ├── svexdb ├── __init__.py ├── wsgi.py ├── urls.py ├── local_settings_example.py └── settings.py ├── README.md ├── .gitattributes ├── static ├── img │ ├── favicon.ico │ ├── egg_flair_sprites.png │ ├── ribbon_flair_sprites.png │ └── special_flair_sprites.png ├── js │ ├── nonreddit.js │ └── esv_tabler.js └── css │ └── base.css ├── requirements.txt ├── manage.py ├── .gitignore └── LICENSE /sv/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /svexdb/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # svexdb 2 | -------------------------------------------------------------------------------- /sv/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sv/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sv/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/svexdb/master/static/img/favicon.ico -------------------------------------------------------------------------------- /static/img/egg_flair_sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/svexdb/master/static/img/egg_flair_sprites.png -------------------------------------------------------------------------------- /static/img/ribbon_flair_sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/svexdb/master/static/img/ribbon_flair_sprites.png -------------------------------------------------------------------------------- /static/img/special_flair_sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokemontrades/svexdb/master/static/img/special_flair_sprites.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | argon2-cffi==16.3.0 2 | Django==2.0 3 | django-bootstrap4==0.0.4 4 | django-ratelimit==1.1.0 5 | djangorestframework==3.7.3 6 | praw==5.2.0 7 | psycopg2==2.7.3.2 8 | python-dateutil==2.6.1 9 | pytz==2017.3 10 | regex==2017.12.12 11 | unicodedata2==10.0.0.post2 12 | -------------------------------------------------------------------------------- /sv/helpers.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | def fromtimestamp(timestamp): 5 | import pytz 6 | utc = pytz.timezone('UTC') 7 | if isinstance(timestamp, (int, float)): 8 | dt = datetime.utcfromtimestamp(timestamp) 9 | else: 10 | dt = timestamp 11 | return dt.replace(tzinfo=utc) 12 | -------------------------------------------------------------------------------- /svexdb/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for svexdb project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "svexdb.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "svexdb.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /sv/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class DumpForm(forms.Form): 5 | ph = "KeySAVe / KeySAV2 / KeyBV output goes here" 6 | paste = forms.CharField(widget=forms.Textarea(attrs={'placeholder': ph,}), 7 | label="", 8 | max_length=75000) 9 | GENERATION_CHOICES = ( 10 | ('7', '7 (SM/USUM)'), 11 | ('6', '6 (XY/ORAS)'), 12 | ) 13 | gen_choice = forms.ChoiceField(choices=GENERATION_CHOICES, required=True, label='Generation') 14 | # include_nonreddit = forms.BooleanField(label="Include matches from non-Reddit sources", required=False) 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## min 2 | *.min.js 3 | *.min.css 4 | 5 | ## Python 6 | __pycache__/ 7 | *.py[cod] 8 | *.egg* 9 | .coverage 10 | 11 | ## Project 12 | local_settings.py 13 | public/static 14 | public/media 15 | migrations/ 16 | 17 | ## SQLite3 18 | *.sqlite3 19 | 20 | ## OS 21 | .DS_Store 22 | ._* 23 | Thumbs.db 24 | Desktop.ini 25 | 26 | ## Sphinx 27 | build 28 | 29 | ## Logs 30 | *.log 31 | 32 | ## Editors and IDEs 33 | *.sublime-project 34 | *.sublime-workspace 35 | *.swp 36 | *.swo 37 | .idea/ 38 | *~ 39 | \#*\# 40 | /.emacs.desktop 41 | /.emacs.desktop.lock 42 | .elc 43 | auto-save-list 44 | .buildpath 45 | .project 46 | .settings 47 | .pydevproject 48 | 49 | ## pipenv 50 | Pipfile 51 | Pipfile.lock 52 | -------------------------------------------------------------------------------- /sv/management/commands/initialize_latests.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from sv.models import Latest 3 | 4 | 5 | class Command(BaseCommand): 6 | help = 'Initalizes entries in Latest model for use by comment_scrape and new_submissions' 7 | 8 | def add_arguments(self, parser): 9 | parser.add_argument('subm_id') 10 | parser.add_argument('comment_id') 11 | 12 | def handle(self, *args, **options): 13 | s, x = Latest.objects.get_or_create(id=1) 14 | c, x = Latest.objects.get_or_create(id=2) 15 | if options['subm_id']: 16 | s.latest_id = options['subm_id'] 17 | else: 18 | s.latest_id = '700000' 19 | 20 | if options['comment_id']: 21 | c.latest_id = options['comment_id'] 22 | else: 23 | c.latest_id = 'ddddddd' 24 | 25 | s.save() 26 | c.save() 27 | 28 | if len(Latest.objects.all()) == 2: 29 | print("Success") 30 | else: 31 | print("Failed") 32 | -------------------------------------------------------------------------------- /sv/views_rest.py: -------------------------------------------------------------------------------- 1 | from sv.models import Trainer, TSV 2 | from sv.serializers import TrainerSerializer, TSVSerializer 3 | from rest_framework import renderers, viewsets 4 | from django.http import Http404 5 | 6 | 7 | class TrainersViewSet(viewsets.ReadOnlyModelViewSet): 8 | queryset = Trainer.objects.all().order_by('username') 9 | renderer_classes = [renderers.JSONRenderer] 10 | serializer_class = TrainerSerializer 11 | paginate_by = 200 12 | 13 | 14 | class ShinyValueViewSet(viewsets.ReadOnlyModelViewSet): 15 | renderer_classes = [renderers.JSONRenderer] 16 | serializer_class = TSVSerializer 17 | 18 | def get_queryset(self): 19 | gen = self.kwargs['gen'] 20 | tsv = int(self.kwargs['tsv']) 21 | if tsv >= 4096: 22 | raise Http404 23 | return TSV.objects.tsv_search(tsv, gen) 24 | 25 | 26 | class TrainerViewSet(viewsets.ReadOnlyModelViewSet): 27 | renderer_classes = [renderers.JSONRenderer] 28 | serializer_class = TrainerSerializer 29 | 30 | def get_queryset(self): 31 | username = self.kwargs['username'] 32 | return Trainer.objects.user_search(username) 33 | -------------------------------------------------------------------------------- /sv/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from sv.models import Trainer, TSV 3 | 4 | 5 | # Serializers define the API representation. 6 | class AuxTSVSerializer(serializers.ModelSerializer): # to fix circular dependency in the 2 serializers 7 | class Meta: 8 | model = TSV 9 | fields = ('tsv', 'gen', 'sub_id', 'created', 'last_seen', 'pending', 'completed', 'archived') 10 | 11 | 12 | class TrainerSerializer(serializers.ModelSerializer): 13 | trainer_shiny_values = AuxTSVSerializer(many=True, read_only=True) 14 | 15 | class Meta: 16 | model = Trainer 17 | fields = ('username', 'flair_text', 'flair_class', 'activity', 'trainer_shiny_values') 18 | 19 | 20 | class AuxTrainerSerializer(serializers.ModelSerializer): 21 | class Meta: 22 | model = Trainer 23 | fields = ('username', 'flair_text', 'flair_class', 'activity') 24 | 25 | 26 | class TSVSerializer(serializers.ModelSerializer): 27 | trainer = AuxTrainerSerializer(read_only=True) 28 | 29 | class Meta: 30 | model = TSV 31 | fields = ('trainer', 'tsv', 'gen', 'sub_id', 'created', 'last_seen', 'pending', 'completed', 'archived') 32 | -------------------------------------------------------------------------------- /svexdb/urls.py: -------------------------------------------------------------------------------- 1 | """svexchange URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import include, path, re_path 18 | from sv import views, views_rest 19 | from svexdb.settings import ADMIN_URL 20 | 21 | urlpatterns = [ 22 | path('', views.paste), 23 | path(ADMIN_URL, admin.site.urls), 24 | path('formatter', views.esv_tabler), 25 | re_path('tsv/(?P[6-7])/(?P[0-3][0-9]{3}|[4][0][0-9][0-5])', 26 | views_rest.ShinyValueViewSet.as_view({'get': 'list'})), 27 | re_path('trainer/(?P[-\w]{1,24})/', views_rest.TrainerViewSet.as_view({'get': 'list'})), 28 | #path('trainers/', views_rest.TrainersViewSet.as_view({'get': 'list'})), 29 | ] 30 | 31 | -------------------------------------------------------------------------------- /sv/templates/esv_tabler.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load bootstrap4 %} 3 | {% load sv_extras %} 4 | {% load static from staticfiles %} 5 | {% block link_tabler_class %} active{% endblock %} 6 | {% block base_title %}SVeX.db ESV Table Formatter{% endblock %} 7 | 8 | {% block base_content %} 9 | 10 |
11 |
12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 |
20 | 21 |
22 | 23 | Formats dump output into ESV-only Reddit tables:
24 | # Box 01
25 | | B01 | C1 | C2 | C3 | C4 | C5 | C6 |
26 | |:--:|:--:|:--:|:--:|:--:|:--:|:--:|
27 | |**R1**| ---- | ---- | ---- | ---- | ---- | ---- |
28 | |**R2**| ---- | ---- | ---- | ---- | ---- | ---- |
29 | |**R3**| ---- | ---- | ---- | ---- | ---- | ---- |
30 | |**R4**| ---- | ---- | ---- | ---- | ---- | ---- |
31 | |**R5**| ---- | ---- | ---- | ---- | ---- | ---- | 32 |
33 |
34 |
35 | 36 | 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /sv/templates/sample_body.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap4 %} 2 | Copy/paste the following sample KeySAVe dump output into the form: 3 |
4 | 5 | B01 - 1,1 - Charmander (♂) - Jolly - Solar Power - 8.31.31.31.31.31 - Dragon - [2561]
6 | B01 - 1,2 - Charmander (♂) - Jolly - Blaze - 31.31.31.1.31.31 - Dark - [1261]
7 | B01 - 1,3 - Charmander (♀) - Jolly - Solar Power - 31.31.31.31.31.7 - Dark - [0442]
8 | B01 - 1,4 - Charmander (♂) - Jolly - Solar Power - 31.31.31.31.31.31 - Dark - [3342]
9 | B01 - 1,5 - Charmander (♀) - Jolly - Solar Power - 31.31.31.31.31.16 - Ice - [0142]
10 | B01 - 1,6 - Charmander (♂) - Jolly - Blaze - 31.31.31.31.29.31 - Dark - [2685]
11 | B01 - 2,1 - Charmander (♂) - Jolly - Solar Power - 27.31.31.31.31.31 - Dark - [0997]
12 | B01 - 2,2 - Charmander (♂) - Jolly - Blaze - 31.31.31.31.31.12 - Ice - [4058]
13 | B01 - 2,3 - Charmander (♂) - Timid - Solar Power - 31.31.31.31.31.22 - Ice - [0996]
14 | B01 - 2,4 - Charmander (♂) - Timid - Blaze - 31.31.31.31.31.5 - Dark - [3204]
15 | B01 - 2,5 - Charmander (♀) - Timid - Solar Power - 31.21.31.31.31.31 - Dark - [2752]
16 | B01 - 2,6 - Charmander (♂) - Timid - Solar Power - 31.31.23.31.31.31 - Dark - [1481]
17 | B01 - 3,1 - Charmander (♂) - Timid - Blaze - 31.8.31.31.31.31 - Dragon - [1402]
18 | B01 - 3,2 - Charmander (♀) - Timid - Solar Power - 31.31.31.31.5.31 - Dark - [3801]
19 | B01 - 3,3 - Charmander (♀) - Timid - Blaze - 31.31.31.5.31.31 - Dark - [3522] 20 |
21 |
22 | -------------------------------------------------------------------------------- /sv/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from sv.models import Trainer, TSV, Report, Latest, Nonreddit 3 | # Register your models here. 4 | 5 | 6 | class TSVInline(admin.TabularInline): 7 | model = TSV 8 | 9 | 10 | class TrainerAdmin(admin.ModelAdmin): 11 | list_display = ('username', 'flair_class', 'flair_text', 'activity') 12 | search_fields = ('username',) 13 | inlines = [TSVInline,] 14 | 15 | 16 | class TSVAdmin(admin.ModelAdmin): 17 | list_display = ('trainer', 'tsv', 'gen', 'sub_id', 'completed', 'created', 'archived', 'last_seen', 'pending') 18 | search_fields = ('tsv',) 19 | actions = ['make_archived'] 20 | 21 | def make_archived(modeladmin, request, queryset): 22 | queryset.update(completed=True, archived=True) 23 | make_archived.short_description = 'Mark selected TSVs as archived' 24 | 25 | 26 | class NonredditAdmin(admin.ModelAdmin): 27 | list_display = ('tsv', 'username', 'ign', 'url', 'fc', 'timestamp', 'language', 'source') 28 | search_fields = ('username', 'tsv', 'ign', 'fc') 29 | 30 | 31 | class ReportAdmin(admin.ModelAdmin): 32 | list_display = ('url', 'status', 'handled', 'info', 'created') 33 | search_fields = ('info',) 34 | actions = ['make_handled'] 35 | 36 | def make_handled(modeladmin, request, queryset): 37 | queryset.update(handled=True) 38 | make_handled.short_description = 'Mark selected reports as handled' 39 | 40 | 41 | admin.site.register(Trainer, TrainerAdmin) 42 | admin.site.register(TSV, TSVAdmin) 43 | admin.site.register(Nonreddit, NonredditAdmin) 44 | admin.site.register(Report, ReportAdmin) 45 | admin.site.register(Latest) 46 | -------------------------------------------------------------------------------- /svexdb/local_settings_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Overrides settings.py 3 | """ 4 | 5 | SECRET_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 6 | # you can also use environment variables to store secret values 7 | # import os 8 | # SECRET_KEY = os.environ['SECRET_KEY'] 9 | 10 | # SECURITY WARNING: don't run with debug turned on in production! 11 | DEBUG = True 12 | 13 | # Database 14 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 15 | 16 | DATABASES = { 17 | 'default': { 18 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 19 | 'NAME': 'mydatabase', # Or path to database file if using sqlite3. 20 | 'USER': 'mydatabaseuser', # Not used with sqlite3. 21 | 'PASSWORD': 'mypassword', # Not used with sqlite3. 22 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 23 | 'PORT': '5432', 24 | } 25 | } 26 | 27 | # Cache 28 | # https://docs.djangoproject.com/en/2.0/ref/settings/#caches 29 | 30 | CACHES = { 31 | 'default': { 32 | 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', 33 | 'LOCATION': 'cache_table', 34 | } 35 | } 36 | 37 | ADMIN_URL = 'admin/' # must end with / 38 | 39 | REDDIT = { # LOCAL CREDENTIALS 40 | 'user-agent': 'your user agent', 41 | 'client_id': 'xxxxxxxxxxxxxx', 42 | 'client_secret': 'xxxxxxxxxxxxxxxxxxxxxxxxxxx', 43 | 'refresh_token': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 44 | 45 | 'client_id_alt': 'xxxxxxxxxxxxxx', 46 | 'client_secret_alt': 'xxxxxxxxxxxxxxxxxxxxxxxxxxx', 47 | 'refresh_token_alt': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 48 | } 49 | -------------------------------------------------------------------------------- /static/js/nonreddit.js: -------------------------------------------------------------------------------- 1 | var data = data_; // get json from template 2 | 3 | $( document ).ready(function() { 4 | $('#more').on('show.bs.modal', function(e) { 5 | $('#mod_tbl_body').empty(); 6 | var i = $(e.relatedTarget).data('nridx'); 7 | var j = $(e.relatedTarget).data('nrsubidx'); 8 | var slice = data[i]; 9 | $('#mod_title').text(slice[j]['pkmn']); 10 | function bodyRow(label, info) { 11 | if (info) 12 | if (info == "JPN SV Share Sheet") 13 | return "\n\t\t"+label+" "+info+"\n"; 14 | else if (info == "GameFAQs") 15 | return "\n\t\t"+label+" "+info+"\n"; 16 | else 17 | return "\n\t\t"+label+" "+info+"\n"; 18 | else 19 | return ""; 20 | } 21 | 22 | var tbl_str = "\n\n"; 23 | tbl_str += bodyRow("Source", slice[j]['source']); 24 | tbl_str += bodyRow("Username", slice[j]['username']); 25 | tbl_str += bodyRow("Link", slice[j]['url']); 26 | tbl_str += bodyRow("TSV", slice[j]['tsv']); 27 | tbl_str += bodyRow("FC", slice[j]['fc']); 28 | tbl_str += bodyRow("IGN", slice[j]['ign']); 29 | tbl_str += bodyRow("Info", slice[j]['other']); 30 | tbl_str += bodyRow("Language", slice[j]['lang']); 31 | tbl_str += bodyRow("Timestamp", slice[j]['timestamp']); 32 | tbl_str += "\t\n
\n"; 33 | $('#mod_tbl_body').append(tbl_str); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /sv/natures.py: -------------------------------------------------------------------------------- 1 | brave = ["Brave", # eng 2 | "ゆうかん", # jpn 3 | # same in French 4 | "Audace", # ita 5 | "Mutig", # ger 6 | "Audaz", # esp 7 | "용감"] # kor 8 | 9 | adamant = ["Adamant", # eng 10 | "いじっぱり", # jpn 11 | "Rigide", # fre 12 | "Decisa", # ita 13 | "Hart", # ger 14 | "Firme", # esp 15 | "고집"] # kor 16 | 17 | bold = ["Bold", 18 | "ずぶとい", 19 | "Assuré", 20 | "Sicura", 21 | "Kühn", 22 | "Osado", 23 | "대담"] 24 | 25 | relaxed = ["Relaxed", 26 | "のんき", 27 | "Relax", 28 | "Placida", 29 | "Locker", 30 | "Plácido", 31 | "무사태평"] 32 | 33 | impish = ["Impish", 34 | "わんぱく", 35 | "Malin", 36 | "Scaltra", 37 | "Pfiffig", 38 | "Agitado", 39 | "장난꾸러기"] 40 | 41 | timid = ["Timid", 42 | "おくびょう", 43 | "Timide", 44 | "Timida", 45 | "Scheu", 46 | "Miedoso", 47 | "겁쟁이"] 48 | 49 | jolly = ["Jolly", 50 | "ようき", 51 | "Jovial", 52 | "Allegra", 53 | "Froh", 54 | "Alegre", 55 | "명랑"] 56 | 57 | modest = ["Modest", 58 | "ひかえめ", 59 | "Modeste", 60 | "Modesta", 61 | "Mäßig", 62 | "Modesto", 63 | "조심"] 64 | 65 | quiet = ["Quiet", 66 | "れいせい", 67 | "Discret", 68 | "Quieta", 69 | "Ruhig", 70 | "Manso", 71 | "냉정"] 72 | 73 | calm = ["Calm", 74 | "おだやか", 75 | "Calme", 76 | "Calma", 77 | "Still", 78 | "Sereno", 79 | "차분"] 80 | 81 | sassy = ["Sassy", 82 | "なまいき", 83 | "Malpoli", 84 | "Vivace", 85 | "Forsch", 86 | "Grosero", 87 | "건방"] 88 | 89 | careful = ["Careful", 90 | "しんちょう", 91 | "Prudent", 92 | "Cauta", 93 | "Sacht", 94 | "Cauto", 95 | "신중"] 96 | -------------------------------------------------------------------------------- /static/js/esv_tabler.js: -------------------------------------------------------------------------------- 1 | function formatOutput(resultID, fillerID) { 2 | document.getElementById(resultID).innerHTML = ""; 3 | var dumpString = document.getElementById("dump").value; 4 | 5 | var box_re = /[Bb]\d{2,4}/; 6 | var row_re = /\d,/; 7 | var col_re = /,\d/; 8 | var esv_re = /\d\d\d\d/; 9 | splitString = dumpString.replace(/^\n$/g, "").split("\n"); 10 | 11 | var current_box_num = 0; 12 | var current_row_num = 0; 13 | var current_col_num = 0; 14 | var boxes_store = {}; 15 | 16 | function processLine(line) { 17 | var box_match = box_re.exec(line); 18 | var row_match = row_re.exec(line); 19 | var col_match = col_re.exec(line); 20 | var esv_match = esv_re.exec(line); 21 | 22 | var box_num, row_num, col_num; 23 | 24 | if (box_match) { 25 | box_num = box_match[0]; 26 | if (box_num != current_box_num) { 27 | current_box_num = box_num; 28 | boxes_store[box_num] = []; 29 | } 30 | } 31 | 32 | if (esv_match) { 33 | if (box_match && row_match && col_match) { 34 | row_num = parseInt(row_match[0][0]); 35 | col_num = parseInt(col_match[0].substring(1,3)); 36 | 37 | boxes_store[box_num][(6 * (row_num - 1) + col_num)] = esv_match[0] 38 | } 39 | } 40 | } 41 | 42 | for (var i = 0; i < splitString.length; i++) { 43 | var line = splitString[i]; 44 | processLine(line); 45 | } 46 | 47 | var blank = document.getElementById(fillerID).value; 48 | for (var box in boxes_store) { 49 | var str = "# Box " + box.substring(1,box.length) + "
| " + box + 50 | " | C1 | C2 | C3 | C4 | C5 | C6 |
|:--:|:--:|:--:|:--:|:--:|:--:|:--:|"; 51 | for (var i = 1; i <= 30; i++) { 52 | if (i % 6 == 1) { 53 | r = Math.floor(i / 6) + 1; 54 | str += "
|**R" + r + "**| "; 55 | } 56 | if (boxes_store[box][i]) { 57 | str += boxes_store[box][i] + " | "; 58 | } 59 | else { 60 | str += blank + " | "; 61 | } 62 | } 63 | str += "

"; 64 | document.getElementById(resultID).innerHTML += str; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /sv/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% load bootstrap4 %} 3 | {% bootstrap_javascript %} 4 | {% bootstrap_css %} 5 | 6 | {% load static from staticfiles %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% block bootstrap4_title %}SVeX.db{% endblock %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 46 |
47 | 48 |
49 |
50 | {% block base_content %}django-bootstrap4 template content{% endblock %} 51 |
52 |
53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /sv/views.py: -------------------------------------------------------------------------------- 1 | from django.core.cache import cache 2 | from django.shortcuts import render 3 | from ratelimit.decorators import ratelimit 4 | from sv.models import TSV 5 | from sv.forms import DumpForm 6 | from sv import dump_search 7 | 8 | 9 | def brb(request): 10 | from django.http import HttpResponse 11 | return HttpResponse('Undergoing maintenance
Be back later
Sorry for any inconvenience.') 12 | 13 | 14 | # @ratelimit(key='header:x-forwarded-for', block=True, rate='5/3s') # prod 15 | # @ratelimit(key='ip', block=True, rate='5/3s') # local 16 | def paste(request): 17 | error = False 18 | if request.method == 'GET': 19 | paste_form = DumpForm() 20 | else: 21 | # A POST request: Handle Form Upload 22 | paste_form = DumpForm(request.POST) # Bind data from request.POST into a PostForm 23 | 24 | # If data is valid, proceeds to create a new post and redirect the user 25 | if paste_form.is_valid(): 26 | text = paste_form.cleaned_data['paste'] 27 | gen = paste_form.cleaned_data['gen_choice'] 28 | '''if gen is '7': 29 | flag = False 30 | else: 31 | flag = paste_form.cleaned_data['include_nonreddit']''' 32 | flag = False 33 | 34 | ds = dump_search.DumpSearcher(text, flag, gen) 35 | d = ds.process_text() 36 | 37 | return render(request, 'result.html', {'item_list': d['results'], 38 | 'total': d['total'], 39 | 'unique': d['unique'], 40 | 'exceeded': d['exceeded'], 41 | 'multiples': d['multi'], 42 | 'nonreddit': d['nonreddit'], 43 | 'gen': gen}) 44 | u6 = cache.get('u6', TSV.objects.get_unique_count('6')) 45 | u7 = cache.get('u7', TSV.objects.get_unique_count('7')) 46 | return render(request, 'dump_form.html', {'error': error, 47 | 'paste_area': paste_form, 48 | 'uniques6': u6, 49 | 'uniques7': u7, }) 50 | 51 | 52 | def esv_tabler(request): 53 | return render(request, 'esv_tabler.html') 54 | -------------------------------------------------------------------------------- /sv/templates/dump_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load bootstrap4 %} 3 | {% load sv_extras %} 4 | {% block link_home_class %} active{% endblock %} 5 | {% block base_content %} 6 | 7 |
8 | 9 |
10 |
11 |
12 | {% csrf_token %} 13 | {% bootstrap_form paste_area %} 14 | {% buttons %} 15 | 18 | 21 | {% endbuttons %} 22 |
23 |
24 |
25 | 26 | 51 | 52 | 53 |
54 | 55 | 56 | 72 | {% endblock %} 73 | -------------------------------------------------------------------------------- /sv/management/commands/scraper.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | import praw 3 | import requests 4 | import sys 5 | import time 6 | from . import cmd_helper 7 | from prawcore.exceptions import RequestException 8 | from sv.models import TSV 9 | from svexdb.settings import REDDIT 10 | 11 | 12 | class Command(BaseCommand): 13 | help = 'Searches range of TSV entries to scrape info and/or delete' 14 | r_praw = praw.Reddit(client_id=REDDIT['client_id_alt'], 15 | client_secret=REDDIT['client_secret_alt'], 16 | refresh_token=REDDIT['refresh_token_alt'], 17 | user_agent=REDDIT['user-agent'],) 18 | 19 | def add_arguments(self, parser): 20 | parser.add_argument('gen') 21 | parser.add_argument('start_value') 22 | parser.add_argument('end_value') 23 | 24 | def handle(self, *args, **options): 25 | try: 26 | gen = int(options['gen']) 27 | except ValueError: 28 | gen = 7 # default 29 | try: 30 | start = int(options['start_value']) 31 | except ValueError: 32 | start = 0 # default 33 | try: 34 | end = int(options['end_value']) 35 | except ValueError: 36 | end = 4096 # default 37 | 38 | for i in range(start, end): 39 | print("scraping gen", gen, "tsv", i) 40 | self.scrape_tsv(i, gen) 41 | 42 | def scrape_tsv(self, sv, gen): 43 | sv_list = TSV.objects.tsv_search(sv, gen) 44 | for tr in sv_list: 45 | done = False 46 | while not done: 47 | try: 48 | cmd_helper.scrape_user_tsv(tr, self.r_praw) 49 | done = True 50 | except(RequestException, 51 | praw.exceptions.APIException, 52 | praw.exceptions.ClientException, 53 | requests.exceptions.ConnectionError, 54 | requests.exceptions.HTTPError, 55 | requests.exceptions.ReadTimeout, 56 | requests.packages.urllib3.exceptions.ReadTimeoutError): 57 | err = sys.exc_info()[:2] 58 | print(err) 59 | time.sleep(45) 60 | time.sleep(5) 61 | 62 | sv_list_reloaded = TSV.objects.tsv_search(sv, gen) # has changes resulting from previous loop 63 | for tr in sv_list_reloaded: 64 | cmd_helper.delete_if_inactive(tr) 65 | 66 | 67 | -------------------------------------------------------------------------------- /sv/iv.py: -------------------------------------------------------------------------------- 1 | from sv import natures 2 | import re 3 | 4 | nat_re = re.compile(r"[(]*(\d,\d|\d)[)]* [|-] \w+([-'’]\w+)* (\w+ )*([(].[)] )*[|-] (?P\w+)", re.UNICODE) 5 | 6 | six_iv = re.compile(r"3[01][./]3[01][./]3[01][./]3[01][./]3[01][./]3[01]") 7 | no_atk = re.compile(r"3[01][./]\d{1,2}[./]3[01][./]3[01][./]3[01][./]3[01]") 8 | no_def = re.compile(r"3[01][./]3[01][./]\d{1,2}[./]3[01][./]3[01][./]3[01]") 9 | no_spa = re.compile(r"3[01][./]3[01][./]3[01][./]\d{1,2}[./]3[01][./]3[01]") 10 | no_spdf = re.compile(r"3[01][./]3[01][./]3[01][./]3[01][./]\d{1,2}[./]3[01]") 11 | no_spee = re.compile(r"3[01][./]3[01][./]3[01][./]3[01][./]3[01][./]\d{1,2}") 12 | 13 | 14 | def is_perfect(line): 15 | six = six_iv.search(line) 16 | if six: # 6 iv = perfect no matter what 17 | return True 18 | 19 | m = nat_re.search(line) 20 | if m: # verify that the line contains a nature 21 | nature = m.group('nat') 22 | else: 23 | return False 24 | 25 | #if nature in natures.lonely: 6iv spreads are commented out 26 | # return bool(no_def.search(line)) 27 | if nature in natures.brave: 28 | return bool(no_spee.search(line)) 29 | elif nature in natures.adamant: 30 | return bool(no_spa.search(line)) 31 | #elif nature in natures.naughty: 32 | # return bool(no_spdf.search(line)) 33 | elif nature in natures.bold: 34 | return bool(no_atk.search(line)) 35 | elif nature in natures.relaxed: 36 | return bool(no_spee.search(line)) 37 | elif nature in natures.impish: 38 | return bool(no_spa.search(line)) 39 | #elif nature in natures.lax: 40 | # return bool(no_spdf.search(line)) 41 | elif nature in natures.timid: 42 | return bool(no_atk.search(line)) 43 | #elif nature in natures.hasty: 44 | # return bool(no_def.search(line)) 45 | elif nature in natures.jolly: 46 | return bool(no_spa.search(line)) 47 | #elif nature == "Naive": 48 | # return bool(no_spdf.search(line)) 49 | elif nature in natures.modest: 50 | return bool(no_atk.search(line)) 51 | #elif nature in natures.mild: 52 | # return bool(no_def.search(line)) 53 | elif nature in natures.quiet: 54 | return bool(no_spee.search(line)) 55 | #elif nature in natures.rash: 56 | # return bool(no_spdf.search(line)) 57 | elif nature in natures.calm: 58 | return bool(no_atk.search(line)) 59 | #elif nature in natures.gentle: 60 | # return bool(no_def.search(line)) 61 | elif nature in natures.sassy: 62 | return bool(no_spee.search(line)) 63 | elif nature in natures.careful: 64 | return bool(no_spa.search(line)) 65 | else: 66 | return False 67 | -------------------------------------------------------------------------------- /sv/templatetags/sv_extras.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | register = template.Library() 3 | 4 | 5 | @register.filter 6 | def delta_format(dt): 7 | if dt is not None: 8 | import datetime 9 | time_delta = datetime.datetime.utcnow() - dt.replace(tzinfo=None) 10 | if time_delta.days == 0: 11 | days = "" 12 | elif time_delta.days == 1: 13 | days = "1 day" 14 | else: 15 | days = "%s days" % str(time_delta.days) 16 | 17 | if time_delta.days >= 10: 18 | return days 19 | 20 | if time_delta.seconds // 3600 > 0: 21 | hours = str(time_delta.seconds // 3600) + "h" 22 | else: 23 | hours = "" 24 | 25 | if time_delta.days >= 1: 26 | minutes = "" 27 | else: 28 | minutes = str(time_delta.seconds % 3600 // 60) + "m" 29 | 30 | return "%s %s %s" % (days, hours, minutes,) 31 | else: 32 | return None 33 | 34 | 35 | egg_flairs = ["lucky", "egg", "eevee", "togepi", "manaphy", "torchic", "pichu", "ditto", "eggcup"] 36 | sp_flairs = ["chatot", "atomic", "totodile", "dragonair", "m-altaria-shiny", 37 | "jellicent-shiny", "dragonite", "ughhhhhh", "jude", "magnezone-shiny", 38 | "nsy", "porygon-z-shiny", "porygon", "porygon2", "serperior-shiny", 39 | "slowpoke", "greninja-shiny", "rod", "mudkip-shiny", "alder", "azumarill-shiny", 40 | "nidoking", "magikarp-shiny", "kaph", "rhyhorn-shiny"] 41 | ribbon_flairs = ["cuteribbon", "coolribbon", "beautyribbon", "smartribbon", "toughribbon"] 42 | 43 | 44 | @register.filter 45 | def flair_format(flair_str): 46 | if flair_str is None or flair_str == "default": 47 | return "" 48 | 49 | if flair_str in egg_flairs: 50 | return "egg-flair flair-%s" % flair_str 51 | elif flair_str in sp_flairs: 52 | return "sp-flair flair-%s" % flair_str 53 | elif flair_str in ribbon_flairs: 54 | return "ribbon-flair flair-%s" % flair_str 55 | 56 | return "" 57 | 58 | 59 | @register.filter 60 | def gen_format(gen_str): 61 | if gen_str == '6': 62 | return "6 (XY/ORAS)" 63 | elif gen_str == '7': 64 | return "7 (SM/USUM)" 65 | else: 66 | return gen_str 67 | 68 | 69 | @register.filter 70 | def tsv_url(sub_id, tsv): 71 | return "https://www.reddit.com/r/SVExchange/comments/%s/%s/" % (sub_id, tsv) 72 | 73 | 74 | @register.filter 75 | def rdt_search_url(tsv): 76 | return "https://www.reddit.com/r/SVExchange/search?q=title:%s&restrict_sr=on&sort=new&t=all" % tsv 77 | 78 | 79 | @register.filter 80 | def as_percentage_of(part, whole): 81 | try: 82 | pct = float(part) / whole * 100.0 83 | return "%.1f" % pct + "%" 84 | except (ValueError, ZeroDivisionError): 85 | return "" 86 | -------------------------------------------------------------------------------- /static/css/base.css: -------------------------------------------------------------------------------- 1 | .egg-flair { 2 | display:inline-block; 3 | height:22px; 4 | width:22px; 5 | background:url('../img/egg_flair_sprites.png') no-repeat; 6 | vertical-align: middle; 7 | pointer-events: none; 8 | } 9 | 10 | .flair-lucky { 11 | background-position: 0px 0; 12 | } 13 | 14 | .flair-egg { 15 | background-position: -22px 0; 16 | } 17 | 18 | .flair-eevee { 19 | background-position: -44px 0; 20 | } 21 | 22 | .flair-togepi { 23 | background-position: -66px 0; 24 | } 25 | 26 | .flair-torchic { 27 | background-position: -88px 0; 28 | } 29 | 30 | .flair-pichu { 31 | background-position: -110px 0; 32 | } 33 | 34 | .flair-manaphy { 35 | background-position: -132px 0; 36 | } 37 | 38 | .flair-eggcup { 39 | background-position: -154px 0; 40 | } 41 | 42 | .sp-flair { 43 | display:inline-block; 44 | height:30px; 45 | width:40px; 46 | background:url('../img/special_flair_sprites.png') no-repeat; 47 | vertical-align: middle; 48 | pointer-events: none; 49 | } 50 | 51 | .flair-chatot{background-position:0px 0;margin:-3px -5px 0px -10px} 52 | .flair-atomic{background-position:-40px 0;margin:-3px -9px -1px -8px} 53 | .flair-totodile{background-position:-80px 0;margin:-5px -7px 2px -10px} 54 | .flair-dragonair{background-position:-120px 0;margin:-1px -5px -2px -7px} 55 | .flair-jellicent-shiny{background-position:-160px 0;margin:-1px -3px -1px -5px} 56 | .flair-m-altaria-shiny{background-position:-200px 0;margin:-3px -2px -1px -7px} 57 | .flair-dragonite{background-position:-240px 0;margin:-1px -4px 0 -6px} 58 | .flair-ughhhhhh{background-position:-280px 0;margin:-5px -3px -2px -7px} 59 | .flair-jude{background-position:-320px 0;margin:-2px -1px -1px -4px} 60 | .flair-magnezone-shiny{background-position:-360px 0;margin:-1px -3px 0 -5px} 61 | .flair-nsy{background-position:-400px 0;margin:0px -5px -1px -8px} 62 | .flair-porygon-z-shiny{background-position:-440px 0;margin:-2px -5px 0 -9px} 63 | .flair-porygon{background-position:-480px 0;margin:-5px -12px 0px -10px} 64 | .flair-porygon2{background-position:-520px 0;margin:-5px -5px 0px -10px} 65 | .flair-serperior-shiny{background-position:-560px 0;margin:-1px -4px -2px -4px} 66 | .flair-slowpoke{background-position:-600px 0;margin:-5px -6px 0px -9px} 67 | .flair-greninja-shiny{background-position:-640px 0;margin:-5px 0px 1px -4px} 68 | .flair-rod{background-position:-680px 0;margin:-1px -3px -3px -8px} 69 | .flair-mudkip-shiny{background-position:-720px 0;margin:-2px -6px -4px -8px} 70 | .flair-alder{background-position:-760px 0;margin:-2px -6px -1px -10px} 71 | .flair-azumarill-shiny{background-position:-800px 0;margin:0 -2px -1px -8px} 72 | .flair-nidoking{background-position:-840px 0;margin:0 0 -3px -3px} 73 | .flair-magikarp-shiny{background-position:-880px 0;margin:-1px -5px 0 -9px} 74 | .flair-kaph{background-position:-920px 0;margin:-1px -5px 0 -9px} 75 | .flair-rhyhorn-shiny{background-position:-960px 0;margin:-3px -5px -2px -7px} 76 | 77 | .ribbon-flair { 78 | display:inline-block; 79 | height:22px; 80 | width:22px; 81 | background:url('../img/ribbon_flair_sprites.png') no-repeat; 82 | vertical-align: middle; 83 | pointer-events: none; 84 | } 85 | 86 | .flair-cuteribbon{background-position:0 0;margin-right:-2px} 87 | .flair-coolribbon{background-position:-22px 0;margin:0 -1px 2px 1px} 88 | .flair-beautyribbon{background-position:-44px 0;margin-left:3px} 89 | .flair-smartribbon{background-position:-66px 0;margin:0 0 -2px 3px} 90 | .flair-toughribbon{background-position:-88px 0;margin:0 0 -2px 3px} 91 | 92 | 93 | /* Not flair related */ 94 | #id_paste { 95 | height:60vh 96 | } 97 | .fa-archive, .fa-external-link-square, .fa-search { 98 | font-size: 120% 99 | } 100 | -------------------------------------------------------------------------------- /sv/management/commands/new_submissions.py: -------------------------------------------------------------------------------- 1 | from django.core.cache import cache 2 | from django.core.management.base import BaseCommand, CommandError 3 | from .cmd_helper import is_from_tsv_thread 4 | from .cmd_helper import get_gen_from_flair_class 5 | from sv.models import Latest, TSV, Report 6 | from svexdb.settings import REDDIT 7 | from prawcore import RequestException 8 | import praw 9 | import requests 10 | import sys 11 | import time 12 | 13 | 14 | class Command(BaseCommand): 15 | help = 'Gets latest submissions and adds/updates new TSV threads' 16 | r = praw.Reddit(client_id=REDDIT['client_id'], 17 | client_secret=REDDIT['client_secret'], 18 | refresh_token=REDDIT['refresh_token'], 19 | user_agent=REDDIT['user-agent'],) 20 | 21 | def handle(self, *args, **options): 22 | done = False 23 | while not done: 24 | try: 25 | self.new_submissions() 26 | done = True 27 | except(RequestException, 28 | praw.exceptions.APIException, 29 | praw.exceptions.ClientException, 30 | requests.exceptions.ConnectionError, 31 | requests.exceptions.HTTPError, 32 | requests.exceptions.ReadTimeout, 33 | requests.packages.urllib3.exceptions.ReadTimeoutError): 34 | err = sys.exc_info()[:2] 35 | self.stderr.write(str(err)) 36 | time.sleep(45) 37 | 38 | def new_submissions(self): 39 | stop_id = Latest.objects.get_latest_tsv_thread_id() 40 | generator = self.r.subreddit('SVExchange').new(limit=50) 41 | 42 | for i, sub in enumerate(generator): 43 | if i == 0: 44 | Latest.objects.set_latest_tsv_thread_id(sub.id) 45 | 46 | if sub.id <= stop_id: 47 | from datetime import datetime 48 | self.stdout.write("new_submissions [Stop] " + str(datetime.utcnow())) 49 | break 50 | elif is_from_tsv_thread(sub.title, sub.link_flair_css_class): 51 | op = sub.author.name 52 | sv = int(sub.title) 53 | gen = get_gen_from_flair_class(sub.link_flair_css_class) 54 | info_str = "%s %s %s %s" % (op, sub.title, gen, sub.id) 55 | 56 | should_update = True 57 | if TSV.objects.check_if_exists(op, sv, gen): # update existing TSV entry 58 | self.stdout.write("Updated? " + info_str) 59 | existing = TSV.objects.get_user_tsv(op, sv, gen) 60 | from sv.helpers import fromtimestamp 61 | dt_new = fromtimestamp(sub.created_utc) 62 | delta = dt_new - existing.created 63 | if sub.id > existing.sub_id and delta.days < 60: 64 | should_update = False 65 | x = "Creating new threads too soon? u/%s - old thread: %s" % (op, existing.sub_id) 66 | Report.objects.create_automated_report(sub.id, x) 67 | else: 68 | uniq_str = " *UNIQUE*" if len(TSV.objects.tsv_search(sv, gen)) == 0 else "" 69 | self.stdout.write("Added " + info_str + uniq_str) 70 | 71 | if should_update: 72 | TSV.objects.update_or_create_user_tsv(op, sub.author_flair_text, sub.author_flair_css_class, sv, 73 | gen, sub.id, False, False, sub.created_utc, sub.created_utc, 74 | None) 75 | mult = 60 + 5 # minutes until cache invalidates. assuming hourly cron job 76 | cache.set('u6', TSV.objects.get_unique_count('6'), 60 * mult) 77 | cache.set('u7', TSV.objects.get_unique_count('7'), 60 * mult) 78 | -------------------------------------------------------------------------------- /sv/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from sv import managers 3 | from sv.helpers import fromtimestamp 4 | import sys 5 | 6 | 7 | # Create your models here. 8 | class Trainer(models.Model): 9 | username = models.CharField(max_length=24, unique=True) 10 | flair_class = models.CharField(max_length=50, null=True, blank=True) 11 | flair_text = models.CharField(max_length=65, null=True, blank=True) 12 | activity = models.DateTimeField(null=True, default=None, blank=True) 13 | 14 | objects = managers.TrainerManager() 15 | 16 | def set_activity(self, timestamp): 17 | dt = fromtimestamp(timestamp) 18 | if not self.activity or self.activity < dt: 19 | self.activity = dt 20 | self.save() 21 | 22 | if sys.version_info >= (3, 0): 23 | def __str__(self): 24 | return self.username 25 | else: 26 | def __unicode__(self): 27 | return self.username 28 | 29 | 30 | class TSV(models.Model): 31 | trainer = models.ForeignKey(Trainer, related_name='trainer_shiny_values', on_delete=models.CASCADE) 32 | tsv = models.PositiveSmallIntegerField() 33 | sub_id = models.CharField(max_length=10) 34 | completed = models.BooleanField(default=False) 35 | archived = models.BooleanField(default=False) 36 | created = models.DateTimeField() 37 | last_seen = models.DateTimeField("most recent op comment") 38 | pending = models.DateTimeField("oldest unreplied comment", default=None, null=True, blank=True) 39 | GENERATION_CHOICES = ( 40 | ('6', '6'), 41 | ('7', '7') 42 | ) 43 | gen = models.CharField(max_length=2, choices=GENERATION_CHOICES) 44 | 45 | objects = managers.TSVManager() 46 | 47 | if sys.version_info >= (3, 0): 48 | def __str__(self): 49 | return self.trainer.username + " " + str(self.tsv) + " " + str(self.sub_id) 50 | else: 51 | def __unicode__(self): 52 | return self.trainer.username + " " + str(self.tsv) + " " + str(self.sub_id) 53 | 54 | 55 | class Nonreddit(models.Model): 56 | username = models.CharField(max_length=75, default=None, blank=True, null=True) 57 | tsv = models.CharField(max_length=4) 58 | fc = models.CharField(max_length=50, default=None, blank=True, null=True) 59 | ign = models.CharField(max_length=50, default=None, blank=True, null=True) 60 | url = models.CharField(max_length=150, default=None, blank=True, null=True) 61 | timestamp = models.CharField(max_length=30, default=None, blank=True, null=True) 62 | language = models.CharField(max_length=30, default=None, blank=True, null=True) 63 | other = models.CharField(max_length=300, default=None, blank=True, null=True) 64 | source = models.CharField(max_length=30, default=None, blank=True, null=True) 65 | 66 | objects = managers.NonredditManager() 67 | 68 | if sys.version_info >= (3, 0): 69 | def __str__(self): 70 | return str(self.source) + " " + str(self.tsv) + " " + str(self.url) 71 | else: 72 | def __unicode__(self): 73 | return str(self.source) + " " + str(self.tsv) + " " + str(self.url) 74 | 75 | 76 | class Report(models.Model): 77 | submitter_ip = models.GenericIPAddressField(blank=True, null=True) # not really used anymore 78 | url = models.URLField() 79 | ACTIVITY_CHOICES = ( 80 | ('active', 'Active'), 81 | ('inactive', 'Inactive'), 82 | ('deleted', 'Deleted'), 83 | ('banned', 'Banned') 84 | ) 85 | status = models.CharField(max_length=8, choices=ACTIVITY_CHOICES) 86 | info = models.CharField(max_length=100, blank=True, null=True) 87 | handled = models.BooleanField(default=False) 88 | created = models.DateTimeField(auto_now_add=True) 89 | 90 | objects = managers.ReportManager() 91 | 92 | if sys.version_info >= (3, 0): 93 | def __str__(self): 94 | return self.url 95 | else: 96 | def __unicode__(self): 97 | return self.url 98 | 99 | 100 | class Latest(models.Model): 101 | latest_id = models.CharField(max_length=50) 102 | 103 | objects = managers.LatestManager() 104 | -------------------------------------------------------------------------------- /sv/management/commands/searcher.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | import datetime 3 | import praw 4 | import sys 5 | import time 6 | from sv.models import TSV 7 | from svexdb.settings import REDDIT 8 | from .cmd_helper import get_gen_from_flair_class, is_from_tsv_thread 9 | 10 | 11 | class Command(BaseCommand): 12 | help = 'Searches subreddit for a specified range of TSV entries to be added' 13 | r = praw.Reddit(client_id=REDDIT['client_id_alt'], 14 | client_secret=REDDIT['client_secret_alt'], 15 | refresh_token=REDDIT['refresh_token_alt'], 16 | user_agent=REDDIT['user-agent'],) 17 | users = [] 18 | 19 | def handle(self, *args, **options): 20 | if sys.version_info >= (3, 0): 21 | start = int(input('Enter start of range: ')) 22 | end = int(input('Enter end of range: ')) 23 | else: # python 2 support 24 | start = int(raw_input('Enter start of range: ')) 25 | end = int(raw_input('Enter end of range: ')) 26 | 27 | for i in range(start, end): 28 | self.search_reddit(i) 29 | self.users = [] # reset 30 | time.sleep(3) 31 | 32 | def search_reddit(self, sv): 33 | zero_padded_sv = str(sv).zfill(4) 34 | query = 'nsfw:no AND title:' + zero_padded_sv 35 | search_results = self.r.subreddit('SVExchange').search(query, sort='new', time_filter='year') 36 | for subm in search_results: 37 | if is_from_tsv_thread(subm.title, subm.link_flair_css_class): 38 | self.process_search_entry(subm) 39 | 40 | def process_search_entry(self, subm): 41 | op = subm.author.name 42 | sv = int(subm.title) 43 | pair = (op, sv) 44 | gen = get_gen_from_flair_class(subm.link_flair_css_class) 45 | self.stdout.write("search result: " + subm.title + " by " + op) 46 | if TSV.objects.check_if_exists(op, sv, gen): 47 | self.stdout.write("\tdupe") 48 | else: 49 | if subm.archived: 50 | self.stdout.write("\tarchived") 51 | return # reached an archived submission. any submission thereafter is also archived 52 | elif pair in self.users: 53 | self.stdout.write("\talready searched and added newer thread") 54 | else: # check the thread 55 | thread = self.r.submission(subm.id) 56 | thread.comment_limit = 40 57 | thread.comment_sort = 'new' 58 | time.sleep(3) 59 | thread.comments.replace_more(limit=1) 60 | flattened_comments = thread.comments.list() 61 | should_add = len(flattened_comments) == 0 or not self.is_too_old(thread.created_utc, 60) # 2 months 62 | latest_timestamp = thread.created_utc # placeholder 63 | 64 | for fc in flattened_comments: 65 | if fc.is_submitter: 66 | if not self.is_too_old(fc.created_utc, 150): 67 | # add if op activity within last 150 days 68 | self.stdout.write("\t\tshould_add %s" % should_add) 69 | should_add = True 70 | if fc.created_utc > latest_timestamp: 71 | latest_timestamp = fc.created_utc 72 | break 73 | 74 | if should_add: 75 | TSV.objects.update_or_create_user_tsv(op, thread.author_flair_text, 76 | thread.author_flair_css_class, sv, 77 | gen, thread.id, False, False, thread.created_utc, 78 | latest_timestamp, None) 79 | self.stdout.write("\t\tadding " + str(sv) + " *********************") 80 | self.users.append(op) 81 | 82 | def is_too_old(self, ts, threshold): 83 | dt = datetime.datetime.fromtimestamp(ts) 84 | # dt is a datetime object. threshold is int of days 85 | dtr = dt.replace(tzinfo=None) 86 | delta = datetime.datetime.utcnow() - dtr 87 | self.stdout.write("\t\tis_too_old %s" % str(delta.days > threshold)) 88 | return delta.days > threshold 89 | -------------------------------------------------------------------------------- /sv/management/commands/new_comments.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from sv.models import Latest, Trainer, TSV 3 | from . import cmd_helper 4 | from svexdb.settings import REDDIT 5 | from prawcore.exceptions import RequestException 6 | import praw 7 | import requests 8 | import sys 9 | import time 10 | import unicodedata2 11 | 12 | 13 | class Command(BaseCommand): 14 | help = 'Crawls latest comments to update last seen data' 15 | r_praw = praw.Reddit(client_id=REDDIT['client_id'], 16 | client_secret=REDDIT['client_secret'], 17 | refresh_token=REDDIT['refresh_token'], 18 | user_agent=REDDIT['user-agent'],) 19 | 20 | def handle(self, *args, **options): 21 | stopping_id = Latest.objects.get_latest_comment_id() 22 | done = False 23 | while not done: 24 | try: 25 | new_stopping_id = self.process_latest_comments(stopping_id) 26 | Latest.objects.set_latest_comment_id(new_stopping_id) 27 | done = True 28 | except(RequestException, 29 | praw.exceptions.APIException, 30 | praw.exceptions.ClientException, 31 | requests.exceptions.ConnectionError, 32 | requests.exceptions.HTTPError, 33 | requests.exceptions.ReadTimeout, 34 | requests.packages.urllib3.exceptions.ReadTimeoutError): 35 | err = sys.exc_info()[:2] 36 | self.stderr.write(str(err)) 37 | time.sleep(45) 38 | 39 | def process_latest_comments(self, stopping_id): 40 | new_stopping_id = stopping_id 41 | COMMENTS_LIMIT = 100 # intended for a cron job every 15 minutes 42 | comments = self.r_praw.subreddit('SVExchange').comments(limit=COMMENTS_LIMIT) 43 | user_tsv_set = set([]) # stores user/tsv combo to avoid duplicates 44 | user_set = set() # stores users to avoid duplicates 45 | for i, c in enumerate(comments): 46 | if i == 0: 47 | new_stopping_id = c.id 48 | 49 | link_title_ascii = unicodedata2.normalize('NFKD', c.link_title).encode('ascii', 'ignore').decode('ascii') 50 | self.stdout.write("%s %s %s" % (c.id, c.link_author.ljust(24), link_title_ascii)) 51 | if c.id <= stopping_id: 52 | from datetime import datetime 53 | self.stdout.write("new_comments [Stop] " + str(datetime.utcnow())) 54 | break 55 | op = c.link_author 56 | commenter = c.author.name 57 | if c.is_submitter and cmd_helper.is_from_tsv_thread(c.link_title): 58 | user_tsv_tuple = (op, c.link_title) 59 | tsv = int(c.link_title) 60 | ts = c.created_utc 61 | if user_tsv_tuple in user_tsv_set: 62 | self.stdout.write("\tRepeat") 63 | elif TSV.objects.check_if_exists(op, tsv): 64 | self.stdout.write("\tUpdating") 65 | new_sub_id = cmd_helper.get_id_from_full_url(c.link_url) 66 | # comment lacks gen info that's found in submission flair 67 | gen = cmd_helper.get_gen_from_comment(op, tsv, new_sub_id, self.r_praw) 68 | user_tsv = TSV.objects.get_user_tsv(op, tsv, gen) 69 | # check if submission id should be updated, in case db doesn't have user's latest thread 70 | old_sub_id = user_tsv.sub_id 71 | 72 | if new_sub_id > old_sub_id: 73 | user_tsv.sub_id = new_sub_id 74 | user_tsv.save() 75 | 76 | cmd_helper.scrape_user_tsv(user_tsv, self.r_praw, ts) 77 | else: 78 | self.stdout.write("\tAdding?") 79 | sub_id = cmd_helper.get_id_from_full_url(c.link_url) 80 | subm = self.r_praw.submission(id=sub_id) 81 | if not subm.over_18: 82 | self.stdout.write("\tAdd") 83 | gen = cmd_helper.get_gen_from_flair_class(subm.link_flair_css_class) 84 | TSV.objects.update_or_create_user_tsv(op, subm.author_flair_text, subm.author_flair_css_class, 85 | tsv, gen, sub_id, False, False, 86 | subm.created_utc, ts, None) 87 | user_tsv_set.add(user_tsv_tuple) 88 | else: 89 | if commenter not in user_set: 90 | user_set.add(commenter) 91 | tr = Trainer.objects.get_user(commenter) 92 | if tr: 93 | tr.set_activity(c.created_utc) 94 | 95 | return new_stopping_id 96 | -------------------------------------------------------------------------------- /svexdb/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for svexdb project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 16 | 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = '' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = False 26 | 27 | # https://docs.djangoproject.com/en/2.0/ref/settings/#allowed-hosts 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | # Django apps 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 42 | # Third party apps 43 | 'bootstrap4', 44 | 'praw', 45 | 'ratelimit', 46 | 'rest_framework', 47 | 48 | # Local apps 49 | 'sv', 50 | ] 51 | 52 | 53 | # https://docs.djangoproject.com/en/2.0/topics/auth/passwords/#using-argon2-with-django 54 | PASSWORD_HASHERS = [ 55 | 'django.contrib.auth.hashers.Argon2PasswordHasher', 56 | 'django.contrib.auth.hashers.PBKDF2PasswordHasher', 57 | 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', 58 | 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', 59 | 'django.contrib.auth.hashers.BCryptPasswordHasher', 60 | ] 61 | 62 | 63 | # MIDDLEWARE SETTINGS 64 | # See: https://docs.djangoproject.com/en/2.0/ref/settings/#middleware 65 | MIDDLEWARE = ( 66 | 'django.middleware.security.SecurityMiddleware', 67 | 'django.contrib.sessions.middleware.SessionMiddleware', 68 | 'django.middleware.common.CommonMiddleware', 69 | 'django.middleware.csrf.CsrfViewMiddleware', 70 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 71 | 'django.contrib.messages.middleware.MessageMiddleware', 72 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 73 | ) 74 | 75 | ROOT_URLCONF = 'svexdb.urls' 76 | 77 | TEMPLATES = [ 78 | { 79 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 80 | 'DIRS': [], 81 | 'APP_DIRS': True, 82 | 'OPTIONS': { 83 | 'context_processors': [ 84 | 'django.contrib.auth.context_processors.auth', 85 | 'django.template.context_processors.debug', 86 | 'django.template.context_processors.request', 87 | 'django.contrib.auth.context_processors.auth', 88 | 'django.contrib.messages.context_processors.messages', 89 | ], 90 | }, 91 | }, 92 | ] 93 | 94 | WSGI_APPLICATION = 'svexdb.wsgi.application' 95 | 96 | 97 | # Password validation 98 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 99 | 100 | AUTH_PASSWORD_VALIDATORS = [ 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 109 | }, 110 | { 111 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 112 | }, 113 | ] 114 | 115 | 116 | # Internationalization 117 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 118 | 119 | LANGUAGE_CODE = 'en-us' 120 | 121 | TIME_ZONE = 'UTC' 122 | 123 | USE_I18N = True 124 | 125 | USE_L10N = True 126 | 127 | USE_TZ = True 128 | 129 | 130 | # Static files (CSS, JavaScript, Images) 131 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 132 | 133 | STATIC_ROOT = 'staticfiles' 134 | STATIC_URL = '/static/' 135 | STATICFILES_DIRS = ( 136 | os.path.join(BASE_DIR, 'static'), 137 | ) 138 | 139 | 140 | # https://django-bootstrap3.readthedocs.io/en/latest/settings.html 141 | 142 | BOOTSTRAP4 = { 143 | 'jquery_url': '//code.jquery.com/jquery-3.2.1.slim.min.js', 144 | 'base_url': '//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/', 145 | 'include_jquery': True, 146 | } 147 | 148 | 149 | # http://www.django-rest-framework.org/api-guide/settings/ 150 | 151 | REST_FRAMEWORK = { 152 | # Use Django's standard `django.contrib.auth` permissions, 153 | # or allow read-only access for unauthenticated users. 154 | 'DEFAULT_PERMISSION_CLASSES': [ 155 | 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' 156 | ], 157 | 158 | 'DEFAULT_THROTTLE_CLASSES': ( 159 | 'rest_framework.throttling.AnonRateThrottle', 160 | ), 161 | 162 | 'DEFAULT_THROTTLE_RATES': { 163 | 'anon': '40/minute', 164 | } 165 | } 166 | 167 | try: 168 | from .local_settings import * 169 | except ImportError: 170 | pass 171 | 172 | -------------------------------------------------------------------------------- /sv/templates/result.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load bootstrap4 %} 3 | {% load sv_extras %} 4 | {% load static from staticfiles %} 5 | {% block base_title %}SVeX.db results{% endblock %} 6 | 7 | {% block base_content %} 8 | 9 |
10 |
11 | 12 | {% if total %} 13 |
14 | {% else %} 15 |
16 | {% endif %} 17 | Gen {{ gen|gen_format }} - Found {{ unique }} matching egg{{ unique|pluralize }}; 18 | {{ total }} matching trainer{{ total|pluralize }} total. 19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% for item in item_list %} 34 | {% if item.username %} 35 | {% if item.completed %} 36 | 37 | {% else %} 38 | 39 | {% endif %} 40 | {% elif item.source %} 41 | 42 | {% else %} 43 | 44 | {% endif %} 45 | 46 | 50 | 56 | 57 | 58 | 69 | 70 | 71 | {% endfor %} 72 | 73 | 74 |
Dump outputUsernameLast seenPendingTSV
{% if item.iv %}{% endif %} 47 | {{ item.copied_str }} 48 | {% if item.iv %}{% endif %} 49 | {% if item.username %} 51 | {{ item.username }} {% if item.main_flair %}{% endif %}{% if item.ribbon_flair %}{% endif %} 52 | {% elif item.source %} 53 | {{ item.source }} 54 | {% endif %} 55 | {% if item.last_seen %}{{ item.last_seen|delta_format }}{% endif %}{% if item.pending %}{{ item.pending|delta_format }}{% endif %} 59 | {% if item.username %} 60 | {% if item.archived %}{% else %}{% endif %} 61 | {% elif item.source %} 62 | {{ item.sv }} 63 | {% else %} 64 | {% if item.sv %} 65 | 66 | {% endif %} 67 | {% endif %} 68 |
75 |
76 | 77 | 78 | 79 | 80 | {% if exceeded %} 81 |
900 egg limit reached.
82 | {% endif %} 83 | 84 | 85 | 86 | {% if multiples %} 87 |
88 |
89 |
90 | 91 |
Trainers with multiple matches
92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | {% for u in multiples %} 102 | 103 | 104 | 105 | 106 | {% endfor %} 107 | 108 |
UsernameHits
{{ u.username }}{{ u.count }}
109 |
110 |
111 |
112 | {% endif %} 113 |
114 |
115 | 116 | 134 | 135 | 141 | 142 | 143 | {% endblock %} 144 | -------------------------------------------------------------------------------- /sv/dump_search.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | from sv import iv 4 | from sv.models import TSV, Nonreddit 5 | from collections import Counter 6 | 7 | 8 | class DumpSearcher: 9 | def __init__(self, paste_text, flag, generation): 10 | self.incl_nonrdt = flag 11 | self.gen = generation 12 | self.max_exceeded = False 13 | self.MAX_QUERIES_PER_POST = 900 14 | self.non_rdt_array = [] 15 | self.non_rdt_index = 0 16 | self.num_of_queries = 0 17 | self.results_dict_list = [] 18 | self.split_paste_iterable = iter(paste_text.splitlines()) 19 | self.total_egg_matches = 0 20 | self.user_matches = [] 21 | self.unique_egg_matches = 0 22 | 23 | def process_text(self): 24 | for line in self.split_paste_iterable: 25 | if len(line.strip()) != 0: # skip blank lines 26 | # parse ESV from KeySAV output 27 | esv_re_match = re.search('\d\d\d\d', line) 28 | if self.num_of_queries > self.MAX_QUERIES_PER_POST: 29 | max_exceeded = True 30 | break 31 | elif esv_re_match: # the line contains an ESV 32 | esv = esv_re_match.group(0) 33 | self.process_line(esv, line) 34 | else: # the line does not contain an ESV, so just copy the line 35 | no_match = {'copied_str': line,} 36 | self.results_dict_list.append(no_match) 37 | return {'results': self.results_dict_list, 38 | 'gen': self.gen, 39 | 'total': self.total_egg_matches, 40 | 'unique': self.unique_egg_matches, 41 | 'exceeded': self.max_exceeded, 42 | 'multi': self.multiples(self.user_matches), 43 | 'nonreddit': json.dumps(self.non_rdt_array)} 44 | 45 | def process_line(self, esv, line): 46 | perf = iv.is_perfect(line) 47 | query_rdt = TSV.objects.tsv_search(esv, self.gen) 48 | if self.incl_nonrdt: 49 | query_other = Nonreddit.objects.tsv_search(esv) 50 | else: 51 | query_other = None 52 | self.num_of_queries += 1 53 | 54 | # pass along the string if there are no matches 55 | if len(query_rdt) == 0 and (query_other is None or len(query_other) == 0): 56 | self.results_dict_list.append({'copied_str': line, 'iv': perf, 'sv': esv}) 57 | else: 58 | self.unique_egg_matches += 1 59 | for i, q in enumerate(query_rdt): 60 | self.total_egg_matches += 1 61 | if i == 0: # first match for an egg->print , dupes->blank 62 | dump_output = line 63 | else: 64 | dump_output = "" 65 | self.user_matches.append(q.trainer.username) # for multiple matches counter 66 | 67 | flair = self.split_flair(q.trainer.flair_class) 68 | 69 | tbl_row_dict = {'copied_str': dump_output, 70 | 'username': q.trainer.username, 71 | 'sub_id': q.sub_id, 72 | 'sv': esv, 73 | 'iv': perf, 74 | 'completed': q.completed, 75 | 'last_seen': q.last_seen, 76 | 'pending': q.pending, 77 | 'main_flair': flair[0], 78 | 'ribbon_flair': flair[1], 79 | 'flair_text': q.trainer.flair_text, 80 | 'archived': q.archived} 81 | self.results_dict_list.append(tbl_row_dict) 82 | 83 | if self.incl_nonrdt: 84 | nr_arr = [] 85 | for i, q in enumerate(query_other): 86 | if i == 0 and len(query_rdt) == 0: 87 | dump_output = line 88 | else: 89 | dump_output = "" 90 | 91 | tbl_row_dict = {'copied_str': dump_output, 92 | 'source': q['source'], 93 | 'sv': esv, 94 | 'iv': perf, 95 | 'index': self.non_rdt_index, # used for non-reddit modal to find json array entry 96 | 'sub_index': i} 97 | self.results_dict_list.append(tbl_row_dict) 98 | json_dict = {'username': q['username'], 99 | 'tsv': esv, 100 | 'url': q['url'], 101 | 'fc': q['fc'], 102 | 'ign': q['ign'], 103 | 'timestamp': q['timestamp'], 104 | 'lang': q['language'], 105 | 'source': q['source'], 106 | 'other': q['other'], 107 | 'pkmn': line} 108 | nr_arr.append(json_dict) 109 | if len(nr_arr) > 0: 110 | self.non_rdt_array.append(nr_arr) 111 | self.non_rdt_index += 1 112 | 113 | def multiples(self, matches): #list of dicts 114 | matches.sort() 115 | count = Counter(matches) 116 | multiple_matches = [] 117 | for k, v in count.items(): 118 | if v > 1: 119 | multiple_matches.append({'username': k, 'count': v}) 120 | return multiple_matches 121 | 122 | def split_flair(self, full_flair): 123 | if full_flair == 'default' or full_flair is None: 124 | return (None, None) 125 | else: 126 | split_flair_array = full_flair.split(" ") 127 | main_flair = split_flair_array[0] 128 | if main_flair == 'default': 129 | main_flair = None 130 | 131 | if len(split_flair_array) == 2: 132 | ribbon_flair = split_flair_array[1] 133 | else: 134 | ribbon_flair = None 135 | 136 | return (main_flair, ribbon_flair) 137 | -------------------------------------------------------------------------------- /sv/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from sv.helpers import fromtimestamp 3 | 4 | 5 | class TrainerManager(models.Manager): 6 | def user_search(self, username): 7 | return self.filter(username__iexact=username) 8 | 9 | def check_if_exists(self, username): 10 | return self.filter(username=username).exists() 11 | 12 | def get_user(self, username): 13 | return self.filter(username=username).first() 14 | 15 | 16 | class TSVManager(models.Manager): 17 | def get_user_tsv(self, username, tsv, gen=None): 18 | if gen: 19 | return self.filter(trainer__username=username, tsv=tsv, gen=gen).first() 20 | else: 21 | return self.filter(trainer__username=username, tsv=tsv).first() 22 | 23 | def update_or_create_user_tsv(self, username, flair_text, flair_class, sv, 24 | gen, sub_id, completed, archived, created, last_seen, pending): 25 | from sv.models import Trainer 26 | trainer, tr_created = Trainer.objects.get_or_create(username=username, 27 | defaults={'flair_text': flair_text, 28 | 'flair_class': flair_class}) 29 | # Update TSV through new submissions crawler 30 | created_dt = fromtimestamp(created) if created else None 31 | last_seen_dt = fromtimestamp(last_seen) if last_seen else None 32 | pending_dt = fromtimestamp(pending) if pending else None 33 | trainer.set_activity(last_seen_dt) 34 | 35 | tsv, tsv_created = self.update_or_create(trainer=trainer, 36 | tsv=sv, 37 | gen=gen, 38 | defaults={ 39 | 'sub_id': sub_id, 40 | 'completed': completed, 41 | 'archived': archived, 42 | 'created': created_dt, 43 | 'last_seen': last_seen_dt, 44 | 'pending': pending_dt}) 45 | 46 | def update_user_tsv_scrape(self, username, sv, gen, recent_ts, pending_ts, flair_text, flair_class, created_ts): 47 | # recent_ts, pending_ts are float timestamps 48 | from sv.models import Trainer 49 | tr = Trainer.objects.get_user(username) 50 | tr.flair_text = flair_text 51 | tr.flair_class = flair_class 52 | tr.save() 53 | tr.set_activity(recent_ts) 54 | 55 | t = self.get_user_tsv(username, sv, gen) 56 | t.last_seen = fromtimestamp(recent_ts) 57 | t.pending = fromtimestamp(pending_ts) if pending_ts else None 58 | t.created = fromtimestamp(created_ts) 59 | t.save() 60 | 61 | def set_archived(self, username, sv, gen, info=''): 62 | t = self.get_user_tsv(username, sv, gen) 63 | t.archived = True 64 | t.completed = True 65 | t.save() 66 | from sv.models import Report 67 | Report.objects.create_automated_report(t.sub_id, info) 68 | 69 | def set_completed(self, username, sv, gen, completed, info=''): 70 | t = self.get_user_tsv(username, sv, gen) 71 | t.completed = completed 72 | t.save() 73 | from sv.models import Report 74 | Report.objects.create_automated_report(t.sub_id, info) 75 | 76 | def set_pending(self, username, sv, gen, timestamp_int): 77 | t = self.get_user_tsv(username, sv, gen) 78 | if t: 79 | t.pending = fromtimestamp(timestamp_int) 80 | t.save() 81 | 82 | def delete_user_tsv(self, username, sv, gen, info=''): 83 | t = self.get_user_tsv(username, sv, gen) 84 | from sv.models import Report 85 | Report.objects.create_automated_report(t.sub_id, info) 86 | t.delete() 87 | 88 | def tsv_search(self, sv, gen): 89 | if not isinstance(sv, int): # tsv is stored as int in Trainer model 90 | sv = int(sv) 91 | return self.filter(tsv=sv, gen=gen).order_by('trainer__username') 92 | 93 | def check_if_exists(self, username, sv, gen=None): 94 | if gen: 95 | return self.filter(trainer__username=username, tsv=sv, gen=gen).exists() 96 | else: # for comment scrape, which cannot directly know sv gen from only comment properties 97 | return self.filter(trainer__username=username, tsv=sv).exists() 98 | 99 | def tsv_is_not_found(self, sv, gen): 100 | return not self.filter(tsv=sv, gen=gen).exists() 101 | 102 | def get_unique_count(self, gen): 103 | return len(self.filter(gen=gen).order_by('tsv').distinct('tsv')) 104 | 105 | 106 | class NonredditManager(models.Manager): 107 | def tsv_search(self, sv): 108 | if not isinstance(sv, str): # tsv is stored as str in Nonreddit model 109 | sv = str(sv) 110 | return self.filter(tsv=sv).order_by('source').values() 111 | 112 | 113 | class LatestManager(models.Manager): 114 | def get_latest_tsv_thread_id(self): 115 | return self.get(id=1).latest_id 116 | 117 | def set_latest_tsv_thread_id(self, sub_id): 118 | latest = self.get(id=1) 119 | latest.latest_id = sub_id 120 | latest.save() 121 | 122 | def get_latest_comment_id(self): 123 | # kind of hacky: comment id is stored in 2nd row of Latest model 124 | return self.get(id=2).latest_id 125 | 126 | def set_latest_comment_id(self, sub_id): 127 | latest = self.get(id=2) 128 | latest.latest_id = sub_id 129 | latest.save() 130 | 131 | 132 | class ReportManager(models.Manager): 133 | def create_automated_report(self, sub_id, info): 134 | if info.startswith("Deleted"): 135 | status = 'deleted' 136 | elif info.startswith("Banned"): 137 | status = 'banned' 138 | elif info.startswith("Creating"): 139 | status = 'active' 140 | else: 141 | status = 'inactive' 142 | r, r_created = self.update_or_create(submitter_ip='0.0.0.0', 143 | url='reddit.com/'+sub_id, 144 | status=status, 145 | info=info) 146 | -------------------------------------------------------------------------------- /sv/management/commands/cmd_helper.py: -------------------------------------------------------------------------------- 1 | from sv.models import TSV 2 | from sv.helpers import fromtimestamp 3 | import datetime 4 | import praw 5 | import re 6 | import sys 7 | import time 8 | 9 | 10 | def scrape_user_tsv(user_tsv, r_praw, timestamp=None): 11 | if user_tsv.archived: # known to be archived. no need to check 12 | return 13 | 14 | username = user_tsv.trainer.username 15 | tsv = user_tsv.tsv 16 | gen = user_tsv.gen 17 | sub_id = user_tsv.sub_id 18 | 19 | subm = r_praw.submission(id=sub_id) 20 | subm.comment_sort = 'new' 21 | subm.comments.replace_more(limit=1) 22 | 23 | if subm.author is None: # or subm.author == "[deleted]": # deleted by user 24 | TSV.objects.delete_user_tsv(username, tsv, gen, "Deleted by OP %s %s" % (username, tsv)) 25 | elif subm.selftext == '': 26 | TSV.objects.delete_user_tsv(username, tsv, gen, "Deleted by mods %s %s" % (username, tsv)) 27 | elif subm.link_flair_css_class == "banned": 28 | TSV.objects.delete_user_tsv(username, tsv, gen, "Banned by mods %s %s" % (username, tsv)) 29 | else: # thread hasn't been deleted 30 | if user_tsv.completed != subm.over_18 and not subm.archived: 31 | blah = "Marked completed" if subm.over_18 else "Re-opened" 32 | TSV.objects.set_completed(username, tsv, gen, subm.over_18, blah + " %s %s" % (username, tsv)) 33 | if user_tsv.archived != subm.archived: 34 | TSV.objects.set_archived(username, tsv, gen, "Archived %s %s" % (username, tsv)) 35 | 36 | cf = subm.comments # root level comment forest 37 | 38 | sys.stdout.write("* scraping %s by %s\n" % (subm.id, str(subm.author))) 39 | pending_ts = oldest_unreplied_root_comment(cf) 40 | 41 | # get time of latest hatch 42 | flattened_comments = flatten_and_sort_new(cf) 43 | 44 | if timestamp: 45 | # latest (int) timestamp was passed in from realtime scraper 46 | most_recent_ts = timestamp 47 | else: 48 | # latest needs to be found 49 | most_recent_ts = user_tsv.last_seen.timestamp() 50 | 51 | for fc in flattened_comments: 52 | # find op's most recent comment in thread 53 | if fc.is_submitter: 54 | comment_ts = fc.created_utc 55 | if comment_ts > most_recent_ts: 56 | most_recent_ts = comment_ts 57 | 58 | # for debug purposes only 59 | most_rec_dt = fromtimestamp(most_recent_ts) # convert timestamp to datetime 60 | previous_time_dt = user_tsv.last_seen 61 | if most_rec_dt > previous_time_dt: 62 | delta = fromtimestamp(datetime.datetime.utcnow()) - most_rec_dt 63 | between = most_rec_dt - previous_time_dt 64 | sys.stdout.write("\t* %s %s\t%s\t%s\n" % (username.ljust(24), str(tsv).zfill(4), str(delta), str(between))) 65 | 66 | TSV.objects.update_user_tsv_scrape(username, 67 | tsv, 68 | gen, 69 | most_recent_ts, 70 | pending_ts, 71 | subm.author_flair_text, 72 | subm.author_flair_css_class, 73 | subm.created_utc) 74 | time.sleep(5) 75 | 76 | 77 | def oldest_unreplied_root_comment(comment_forest): 78 | if len(comment_forest) == 0: 79 | return None 80 | 81 | oldest_timestamp = None 82 | 83 | for i, root in enumerate(comment_forest): 84 | if comment_exists(root) and not root.is_submitter and not comment_should_be_ignored(root): 85 | replies_forest = root.replies 86 | replies_forest.replace_more(limit=1) # consume any 'continue this thread -->' 87 | sorted_replies = flatten_and_sort_new(replies_forest) 88 | 89 | replied_to = False 90 | for f in sorted_replies: 91 | if f.is_submitter: 92 | replied_to = True 93 | sys.stdout.write("\t* root %s\treplied_to %s\tby %s\n" % (root.id, str(replied_to), f.id)) 94 | break 95 | 96 | if not replied_to: 97 | oldest_timestamp = root.created_utc # found older unreplied root comment 98 | sys.stdout.write("\t* root %s\treplied_to %s\n" % (root.id, replied_to)) 99 | else: 100 | if i == 0: # Most recent comment has been replied to. No need to look further. 101 | return None 102 | break 103 | 104 | return oldest_timestamp 105 | 106 | 107 | def comment_exists(comment): 108 | return (isinstance(comment, praw.models.Comment) and 109 | comment.author is not None) 110 | 111 | 112 | def comment_should_be_ignored(comment): 113 | ignored_users = ['AutoModerator', 'FlairHQ', 'Porygon-Bot'] 114 | return (comment.body.lower().find('giveaway') > -1 or 115 | comment.body.lower().find('claim') > -1 or 116 | comment.author.name in ignored_users or 117 | comment.author_flair_css_class == "banned") 118 | 119 | 120 | def flatten_and_sort_new(comment_forest): 121 | flatten = comment_forest.list() 122 | return sorted(flatten, key=lambda x: x.created_utc, reverse=True) 123 | 124 | 125 | def get_id(url): 126 | # extracts submission id from reddit shortlink (PRAW's get_submission does not support shortlinks) 127 | sub_re = re.compile(r"http://redd.it/(?P\w+)+") 128 | m = sub_re.search(url) 129 | if m: 130 | return m.group('sub_id') 131 | else: 132 | return None 133 | 134 | 135 | def get_id_from_full_url(long_url): 136 | sub_re = re.compile(r"comments/(?P\w+)+/\d\d\d\d") 137 | m = sub_re.search(long_url) 138 | if m: 139 | return m.group('sub_id') 140 | else: 141 | return None 142 | 143 | 144 | def is_from_tsv_thread(title, flair="sv7"): 145 | if re.search('\d\d\d\d', title): 146 | return len(title) == 4 and (flair == "sv6" or flair == "sv7") 147 | else: 148 | return False 149 | 150 | 151 | def get_gen_from_flair_class(flair): 152 | if flair == "sv6": 153 | return "6" 154 | elif flair == "sv7": 155 | return "7" 156 | else: 157 | return "7" 158 | 159 | 160 | def get_gen_from_comment(op, tsv, sub_id, r_praw): 161 | if (TSV.objects.check_if_exists(op, tsv, '6') and TSV.objects.check_if_exists(op, tsv, '7')): 162 | # rare case where a Trainer has the same TSV for both gens -> retrieve gen from submission id 163 | subm = r_praw.submission(id=sub_id) 164 | return get_gen_from_flair_class(subm.link_flair_css_class) 165 | else: 166 | return TSV.objects.get_user_tsv(op, tsv).gen 167 | 168 | 169 | def delete_if_inactive(user_tsv): 170 | pending = user_tsv.pending 171 | if pending: 172 | now = fromtimestamp(datetime.datetime.utcnow()) 173 | delta1 = now - user_tsv.trainer.activity # last seen in subreddit 174 | delta2 = now - pending 175 | threshold = 30 # days 176 | if delta1.days > threshold and delta2.days > threshold: 177 | username = user_tsv.trainer.username 178 | TSV.objects.delete_user_tsv(username, 179 | user_tsv.tsv, 180 | user_tsv.gen, 181 | "Purged from db for inactivity - %s %s" % (username, user_tsv.tsv)) 182 | return 183 | if user_tsv.archived: 184 | now = fromtimestamp(datetime.datetime.utcnow()) 185 | delta = now - user_tsv.created 186 | threshold = 215 # archived has been for 30+ days 187 | if delta.days > threshold: 188 | username = user_tsv.trainer.username 189 | TSV.objects.delete_user_tsv(username, 190 | user_tsv.tsv, 191 | user_tsv.gen, 192 | "Purged from db for nonrenewal - %s %s" % (username, user_tsv.tsv)) 193 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /sv/iv_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from sv import iv 3 | 4 | 5 | class PerfectIVChecker(unittest.TestCase): 6 | def test_keybv(self): 7 | self.assertTrue(iv.is_perfect("~ - 3 - Espurr (♀) - Timid - Keen Eye - 31.26.31.31.31.31 - Dragon - [1977]")) 8 | 9 | def test_keysav1(self): 10 | self.assertFalse(iv.is_perfect("| B18 | 1,3 | Honedge (M) | Adamant | No Guard | 31.31.31.31.01.31 | 0184 |")) 11 | self.assertTrue(iv.is_perfect("B1 - 3,4 - Aipom エイパム (M) - Jolly - Run Away - 31.31.31.26.31.31 [0766] - Electric")) 12 | self.assertFalse(iv.is_perfect("B1 - 3,4 - Aipom エイパム (M) - Jolly - Run Away - 31.31.28.26.31.31 [0766] - Electric")) 13 | self.assertFalse(iv.is_perfect("| B4 | 3,3 | Carbink | Bold | Sturdy | 31.31.31.19.31.31 | 3827 |")) 14 | self.assertTrue(iv.is_perfect("| B4 | 3,3 | Carbink | Bold | Sturdy | 31.01.31.31.31.31 | 3827 |")) 15 | self.assertTrue(iv.is_perfect("| 2,1 | Corsola (F) | Bold | Hustle | 31/08/31/31/31/31 | 1506 | Dragon |")) 16 | self.assertTrue(iv.is_perfect("| B2 | 1,5 | Phantump (F) | Impish | Frisk | 31.31.30.31.31.31 | 3809 |")) 17 | self.assertTrue(iv.is_perfect("| B2 | 1,5 | Phantump (F) | Impish | Frisk | 30.30.30.30.30.30 | 3809 |")) 18 | self.assertFalse(iv.is_perfect("| B2 | 1,5 | Phantump (F) | Impish | Frisk | 30.29.30.31.31.31 | 3809 |")) 19 | 20 | def test_keysav2(self): 21 | self.assertFalse(iv.is_perfect("B01 - 1,5 - Wailmer (♂) - Modest - Water Veil - 31.31.31.31.31.18 - Ice - [2431]")) 22 | self.assertTrue(iv.is_perfect("B04 - 2,6 - Carbink (-) - Bold - Sturdy - 31.28.31.31.31.31 - Dragon - [3439]")) 23 | self.assertTrue(iv.is_perfect("B17 - 2,6 - Salamèche (♂) - Bold - Chlorophyll - 31.0.31.30.31.30 - Fire")) 24 | 25 | 26 | def test_fork(self): 27 | self.assertTrue(iv.is_perfect("| 1,6 | Heracross (F) | Jolly | Swarm | 31/31/31/17/31/31 | [1633] | Love Ball | Dark |")) 28 | 29 | def test_eng(self): 30 | self.assertTrue(iv.is_perfect("B14 - 4,3 - Roggenrola (♂) - Brave - Sand Force - 31.31.31.31.31.1 - Dark - [0009]")) #brave 31 | self.assertTrue(iv.is_perfect("B14 - 3,6 - Pancham (♀) - Adamant - Iron Fist - 31.31.31.19.31.31 - Dark - [0814]")) #adamant 32 | self.assertTrue(iv.is_perfect("B14 - 3,1 - Klefki (♀) - Bold - Magician - 31.4.31.31.31.31 - Dragon - [3914]")) #bold 33 | self.assertTrue(iv.is_perfect("B24 - 4,2 - Porygon (♀) - Relaxed - Analytic - 31.31.31.31.31.11 - Dark - ")) #relaxed 34 | self.assertTrue(iv.is_perfect("B08 - 1,4 - Furfrou (♀) - Impish - Fur Coat - 31.31.31.11.31.31 - Dark - [3345]")) #impish 35 | self.assertTrue(iv.is_perfect("B14 - 1,1 - Noibat (♂) - Timid - Frisk - 31.25.31.31.31.31 - Dark - [2393]")) #timid 36 | self.assertTrue(iv.is_perfect("B10 - 3,6 - Tirtouga (♀) - Jolly - Swift Swim - 31.31.31.20.31.31 - Electric - ")) #jolly 37 | self.assertTrue(iv.is_perfect("B14 - 5,1 - Bulbasaur (♂) - Modest - Chlorophyll - 31.11.31.31.31.31 - Dark - [3455]")) #modest 38 | self.assertTrue(iv.is_perfect("B14 - 5,4 - Amaura (♂) - Quiet - Refrigerate - 31.31.31.31.31.0 - Psychic - [0132]")) #quiet 39 | self.assertTrue(iv.is_perfect("B14 - 4,5 - Goomy (♀) - Calm - Gooey - 31.1.31.31.31.31 - Dark - [1478]")) #calm 40 | self.assertTrue(iv.is_perfect("B27 - 4,1 - Binacle (♀) - Sassy - Pickpocket - 31.31.31.31.31.24 - Ice - ")) #sassy 41 | self.assertTrue(iv.is_perfect("B27 - 3,6 - Ditto (-) - Careful - Limber - 31.31.31.20.31.31 - Dark - ")) #careful 42 | 43 | def test_jpn(self): 44 | self.assertTrue(iv.is_perfect("B14 - 4,3 - ダンゴロ (♂) - ゆうかん - すなのちから - 31.31.31.31.31.1 - あく - [0009]")) #brave 45 | self.assertTrue(iv.is_perfect("B14 - 3,6 - ヤンチャム (♀) - いじっぱり - てつのこぶし - 31.31.31.19.31.31 - あく - [0814]")) #adamant 46 | self.assertTrue(iv.is_perfect("B14 - 3,1 - クレッフィ (♀) - ずぶとい - マジシャン - 31.4.31.31.31.31 - ドラゴン - [3914]")) #bold 47 | self.assertTrue(iv.is_perfect("B24 - 4,2 - ポリゴン (♀) - のんき - アナライズ - 31.31.31.31.31.1 - あく - ")) #relaxed 48 | self.assertTrue(iv.is_perfect("B08 - 1,4 - トリミアン (♀) - わんぱく - ファーコート - 31.31.31.11.31.31 - あく - [3345]")) #impish 49 | self.assertTrue(iv.is_perfect("B14 - 1,1 - オンバット (♂) - おくびょう - おみとおし - 31.25.31.31.31.31 - あく - [2393]")) #timid 50 | self.assertTrue(iv.is_perfect("B10 - 3,6 - プロトーガ (♀) - ようき - すいすい - 31.31.31.20.31.31 - でんき - ")) #jolly 51 | self.assertTrue(iv.is_perfect("B14 - 5,1 - フシギダネ (♂) - ひかえめ - ようりょくそ - 31.11.31.31.31.31 - あく - [3455]")) #modest 52 | self.assertTrue(iv.is_perfect("B14 - 5,4 - アマルス (♂) - れいせい - フリーズスキン - 31.31.31.31.31.0 - エスパー - [0132]")) #quiet 53 | self.assertTrue(iv.is_perfect("B14 - 4,5 - ヌメラ (♀) - おだやか - ぬめぬめ - 31.1.31.31.31.31 - あく - [1478]")) #calm 54 | self.assertTrue(iv.is_perfect("B27 - 4,1 - カメテテ (♀) - なまいき - わるいてぐせ - 31.31.31.31.31.24 - こおり - ")) #sassy 55 | self.assertTrue(iv.is_perfect("B27 - 3,6 - メタモン (-) - しんちょう - じゅうなん - 31.31.31.20.31.31 - あく - ")) #careful 56 | 57 | def test_french(self): 58 | self.assertTrue(iv.is_perfect("B14 - 4,3 - Nodulithe (♂) - Brave - Force Sable - 31.31.31.31.31.1 - Ténèbres - [0009]")) #brave 59 | self.assertTrue(iv.is_perfect("B14 - 3,6 - Pandespiègle (♀) - Rigide - Poing de Fer - 31.31.31.19.31.31 - Ténèbres - [0814]")) #adamant 60 | self.assertTrue(iv.is_perfect("B14 - 3,1 - Trousselin (♀) - Assuré - Magicien - 31.4.31.31.31.31 - Dragon - [3914]")) #bold 61 | self.assertTrue(iv.is_perfect("B24 - 4,2 - Porygon (♀) - Relax - Analyste - 31.31.31.31.31.1 - Ténèbres - ")) #relaxed 62 | self.assertTrue(iv.is_perfect("B08 - 1,4 - Couafarel (♀) - Malin - Toison Épaisse - 31.31.31.11.31.31 - Ténèbres - [3345]")) #impish 63 | self.assertTrue(iv.is_perfect("B14 - 1,1 - Sonistrelle (♂) - Timide - Fouille - 31.25.31.31.31.31 - Ténèbres - [2393]")) #timid 64 | self.assertTrue(iv.is_perfect("B10 - 3,6 - Carapagos (♀) - Jovial - Glissade - 31.31.31.20.31.31 - Électrik - ")) #jolly 65 | self.assertTrue(iv.is_perfect("B14 - 5,1 - Bulbizarre (♂) - Modeste - Chlorophylle - 31.11.31.31.31.31 - Ténèbres - [3455]")) #modest 66 | self.assertTrue(iv.is_perfect("B14 - 5,4 - Amagara (♂) - Discret - Peau Gelée - 31.31.31.31.31.0 - Psy - [0132]")) #quiet 67 | self.assertTrue(iv.is_perfect("B14 - 4,5 - Mucuscule (♀) - Calme - Poisseux - 31.1.31.31.31.31 - Ténèbres - [1478]")) #calm 68 | self.assertTrue(iv.is_perfect("B27 - 4,1 - Opermine (♀) - Malpoli - Pickpocket - 31.31.31.31.31.24 - Glace - ")) #sassy 69 | self.assertTrue(iv.is_perfect("B27 - 3,6 - Métamorph (-) - Prudent - Échauffement - 31.31.31.20.31.31 - Ténèbres - ")) #careful 70 | 71 | def test_ita(self): 72 | self.assertTrue(iv.is_perfect("B14 - 4,3 - Roggenrola (♂) - Audace - Silicoforza - 31.31.31.31.31.1 - Buio - [0009]")) #brave 73 | self.assertTrue(iv.is_perfect("B14 - 3,6 - Pancham (♀) - Decisa - Ferropugno - 31.31.31.19.31.31 - Buio - [0814]")) #adamant 74 | self.assertTrue(iv.is_perfect("B14 - 3,1 - Klefki (♀) - Sicura - Prestigiatore - 31.4.31.31.31.31 - Drago - [3914]")) #bold 75 | self.assertTrue(iv.is_perfect("B24 - 4,2 - Porygon (♀) - Placida - Ponderazione - 31.31.31.31.31.1 - Buio - ")) #relaxed 76 | self.assertTrue(iv.is_perfect("B08 - 1,4 - Furfrou (♀) - Scaltra - Foltopelo - 31.31.31.11.31.31 - Buio - [3345]")) #impish 77 | self.assertTrue(iv.is_perfect("B14 - 1,1 - Noibat (♂) - Timida - Indagine - 31.25.31.31.31.31 - Buio - [2393]")) #timid 78 | self.assertTrue(iv.is_perfect("B10 - 3,6 - Tirtouga (♀) - Allegra - Nuotovelox - 31.31.31.20.31.31 - Elettro - ")) #jolly 79 | self.assertTrue(iv.is_perfect("B14 - 5,1 - Bulbasaur (♂) - Modesta - Clorofilla - 31.11.31.31.31.31 - Buio - [3455]")) #modest 80 | self.assertTrue(iv.is_perfect("B14 - 5,4 - Amaura (♂) - Quieta - Pellegelo - 31.31.31.31.31.0 - Psico - [0132]")) #quiet 81 | self.assertTrue(iv.is_perfect("B14 - 4,5 - Goomy (♀) - Calma - Viscosità - 31.1.31.31.31.31 - Buio - [1478]")) #calm 82 | self.assertTrue(iv.is_perfect("B27 - 4,1 - Binacle (♀) - Vivace - Arraffalesto - 31.31.31.31.31.24 - Ghiaccio - ")) #sassy 83 | self.assertTrue(iv.is_perfect("B27 - 3,6 - Ditto (-) - Cauta - Scioltezza - 31.31.31.20.31.31 - Buio - ")) #careful 84 | 85 | def test_deu(self): 86 | self.assertTrue(iv.is_perfect("B14 - 4,3 - Kiesling (♂) - Mutig - Sandgewalt - 31.31.31.31.31.1 - Unlicht - [0009]")) #brave 87 | self.assertTrue(iv.is_perfect("B14 - 3,6 - Pam-Pam (♀) - Hart - Eisenfaust - 31.31.31.19.31.31 - Unlicht - [0814]")) #adamant 88 | self.assertTrue(iv.is_perfect("B14 - 3,1 - Clavion (♀) - Kühn - Zauberer - 31.4.31.31.31.31 - Drache - [3914]")) #bold 89 | self.assertTrue(iv.is_perfect("B24 - 4,2 - Porygon (♀) - Locker - Analyse - 31.31.31.31.31.1 - Unlicht - ")) #relaxed 90 | self.assertTrue(iv.is_perfect("B08 - 1,4 - Coiffwaff (♀) - Pfiffig - Fellkleid - 31.31.31.11.31.31 - Unlicht - [3345]")) #impish 91 | self.assertTrue(iv.is_perfect("B14 - 1,1 - eF-eM (♂) - Scheu - Schnüffler - 31.25.31.31.31.31 - Unlicht - [2393]")) #timid 92 | self.assertTrue(iv.is_perfect("B10 - 3,6 - Galapaflos (♀) - Froh - Wassertempo - 31.31.31.20.31.31 - Elektro - ")) #jolly 93 | self.assertTrue(iv.is_perfect("B14 - 5,1 - Bisasam (♂) - Mäßig - Chlorophyll - 31.11.31.31.31.31 - Unlicht - [3455]")) #modest 94 | self.assertTrue(iv.is_perfect("B14 - 5,4 - Amarino (♂) - Ruhig - Frostschicht - 31.31.31.31.31.0 - Psycho - [0132]")) #quiet 95 | self.assertTrue(iv.is_perfect("B14 - 4,5 - Viscora (♀) - Still - Viskosität - 31.1.31.31.31.31 - Unlicht - [1478]")) #calm 96 | self.assertTrue(iv.is_perfect("B27 - 4,1 - Bithora (♀) - Forsch - Langfinger - 31.31.31.31.31.24 - Eis - ")) #sassy 97 | self.assertTrue(iv.is_perfect("B27 - 3,6 - Ditto (-) - Sacht - Flexibilität - 31.31.31.20.31.31 - Unlicht - ")) #careful 98 | 99 | def test_esp(self): 100 | self.assertTrue(iv.is_perfect("B14 - 4,3 - Roggenrola (♂) - Audaz - Poder Arena - 31.31.31.31.31.1 - Siniestro - [0009]")) #brave 101 | self.assertTrue(iv.is_perfect("B14 - 3,6 - Pancham (♀) - Firme - Puño Férreo - 31.31.31.19.31.31 - Siniestro - [0814]")) #adamant 102 | self.assertTrue(iv.is_perfect("B14 - 3,1 - Klefki (♀) - Osado - Prestidigitador - 31.4.31.31.31.31 - Dragón - [3914]")) #bold 103 | self.assertTrue(iv.is_perfect("B24 - 4,2 - Porygon (♀) - Plácido - Cálculo Final - 31.31.31.31.31.1 - Siniestro - ")) #relaxed 104 | self.assertTrue(iv.is_perfect("B08 - 1,4 - Furfrou (♀) - Agitado - Pelaje Recio - 31.31.31.11.31.31 - Siniestro - [3345]")) #impish 105 | self.assertTrue(iv.is_perfect("B14 - 1,1 - Noibat (♂) - Miedoso - Cacheo - 31.25.31.31.31.31 - Siniestro - [2393]")) #timid 106 | self.assertTrue(iv.is_perfect("B10 - 3,6 - Tirtouga (♀) - Alegre - Nado Rápido - 31.31.31.20.31.31 - Eléctrico - ")) #jolly 107 | self.assertTrue(iv.is_perfect("B14 - 5,1 - Bulbasaur (♂) - Modesto - Clorofila - 31.11.31.31.31.31 - Siniestro - [3455]")) #modest 108 | self.assertTrue(iv.is_perfect("B14 - 5,4 - Amaura (♂) - Manso - Piel Helada - 31.31.31.31.31.0 - Psíquico - [0132]")) #quiet 109 | self.assertTrue(iv.is_perfect("B14 - 4,5 - Goomy (♀) - Sereno - Baba - 31.1.31.31.31.31 - Siniestro - [1478]")) #calm 110 | self.assertTrue(iv.is_perfect("B27 - 4,1 - Binacle (♀) - Grosero - Hurto - 31.31.31.31.31.24 - Hielo - ")) #sassy 111 | self.assertTrue(iv.is_perfect("B27 - 3,6 - Ditto (-) - Cauto - Flexibilidad - 31.31.31.20.31.31 - Siniestro - ")) #careful 112 | 113 | def test_kor(self): 114 | self.assertTrue(iv.is_perfect("B14 - 4,3 - 단굴 (♂) - 용감 - 모래의힘 - 31.31.31.31.31.1 - 악 - [0009]")) #brave 115 | self.assertTrue(iv.is_perfect("B14 - 3,6 - 판짱 (♀) - 고집 - 철주먹 - 31.31.31.19.31.31 - 악 - [0814]")) #adamant 116 | self.assertTrue(iv.is_perfect("B14 - 3,1 - 클레피 (♀) - 대담 - 매지션 - 31.4.31.31.31.31 - 드래곤 - [3914]")) #bold 117 | self.assertTrue(iv.is_perfect("B24 - 4,2 - 폴리곤 (♀) - 무사태평 - 애널라이즈 - 31.31.31.31.31.1 - 악 - ")) #relaxed 118 | self.assertTrue(iv.is_perfect("B08 - 1,4 - 트리미앙 (♀) - 장난꾸러기 - 퍼코트 - 31.31.31.11.31.31 - 악 - [3345]")) #impish 119 | self.assertTrue(iv.is_perfect("B14 - 1,1 - 음뱃 (♂) - 겁쟁이 - 통찰 - 31.25.31.31.31.31 - 악 - [2393]")) #timid 120 | self.assertTrue(iv.is_perfect("B10 - 3,6 - 프로토가 (♀) - 명랑 - 쓱쓱 - 31.31.31.20.31.31 - 전기 - ")) #jolly 121 | self.assertTrue(iv.is_perfect("B14 - 5,1 - 이상해씨 (♂) - 조심 - 엽록소 - 31.11.31.31.31.31 - 악 - [3455]")) #modest 122 | self.assertTrue(iv.is_perfect("B14 - 5,4 - 아마루스 (♂) - 냉정 - 프리즈스킨 - 31.31.31.31.31.0 - 에스퍼 - [0132]")) #quiet 123 | self.assertTrue(iv.is_perfect("B14 - 4,5 - 미끄메라 (♀) - 차분 - 미끈미끈 - 31.1.31.31.31.31 - 악 - [1478]")) #calm 124 | self.assertTrue(iv.is_perfect("B27 - 4,1 - 거북손손 (♀) - 건방 - 나쁜손버릇 - 31.31.31.31.31.24 - 얼음 - ")) #sassy 125 | self.assertTrue(iv.is_perfect("B27 - 3,6 - 메타몽 (-) - 신중 - 유연 - 31.31.31.20.31.31 - 악 - ")) #careful 126 | 127 | def test_vivillon(self): 128 | self.assertTrue(iv.is_perfect("B01 - 1,1 - Vivillon-High Plains (♀) - Timid - Friend Guard - 31.9.31.31.31.31 - Ghost - ")) 129 | self.assertFalse(iv.is_perfect("B01 - 1,1 - Vivillon-High Plains (♀) - Timid - Friend Guard - 31.9.31.31.15.31 - Ghost - ")) 130 | self.assertTrue(iv.is_perfect("B01 - 5,6 - Vivillon-Fancy (♀) - Calm - Compound Eyes - 31.1.31.31.31.31 - Bug -")) 131 | self.assertTrue(iv.is_perfect("B01 - 5,6 - ビビヨン-ファンシーなもよう (♀) - おだやか - ふくがん - 31.1.31.31.31.31 - むし - ")) 132 | self.assertFalse(iv.is_perfect("B01 - 5,6 - Vivillon-Fancy (♀) - Calm - Compound Eyes - 25.31.31.17.28.16 - Bug -")) 133 | 134 | def test_farfetchd(self): 135 | self.assertTrue(iv.is_perfect("B02 - 2,2 - Farfetch’d (♂) - Jolly - Keen Eye - 31.31.31.2.31.31 - Electric - [0503]")) 136 | self.assertTrue(iv.is_perfect("B02 - 2,2 - Farfetch'd (♂) - Jolly - Keen Eye - 31.31.31.2.31.31 - Electric - [0503]")) 137 | 138 | 139 | if __name__ == '__main__': 140 | unittest.main() 141 | --------------------------------------------------------------------------------