yes am pro web dev yes

+ tweaked a few commands
+ added exception raised from the commands in the docs
+ added models to the documentation
+ Now using Bootstrap with the Superhero theme
+ Replaced side navbar with a top navbar
+ Added global search that works like the find command
+ Added a templatetag for generating the navbar
+ Layout likely to change again, but at least it has a nice theme now...
doc_update
Joey Hines 2019-01-11 16:40:12 -06:00
parent 0dbb2c95a4
commit 6cdfe76505
17 changed files with 11734 additions and 91 deletions

View File

@ -4,6 +4,7 @@ from GeoffreyApp.errors import *
from GeoffreyApp.minecraft_api import *
import inspect
import datetime
import re
command_dict = {"GET": {}, "POST": {}, "DELETE": {}}
@ -35,12 +36,15 @@ def command(type):
def get_player(discord_uuid=None, mc_uuid=None):
if discord_uuid is not None:
player = Player.objects.get(discord_uuid=discord_uuid)
elif mc_uuid is not None:
player = Player.objects.get(mc_uuid=discord_uuid)
else:
raise AttributeError
try:
if discord_uuid is not None:
player = Player.objects.get(discord_uuid__iexact=discord_uuid)
elif mc_uuid is not None:
player = Player.objects.get(mc_uuid__iexact=mc_uuid)
else:
raise AttributeError
except Player.DoesNotExist:
raise PlayerNotFound
return player
@ -71,13 +75,13 @@ def add_location(x_pos, z_pos, name=None, discord_uuid=None, mc_uuid=None, loc_t
try:
get_location(player, name, loc_type=loc_type)
raise EntryNameNotUniqueError
except (Location.DoesNotExist, NoLocationsInDatabase):
except (Location.DoesNotExist, NoLocationsInDatabase, LocationLookUpError):
if name is None:
name = "{}'s {}".format(player.name, loc_type.__name__)
location = loc_type.objects.create(owner=player, name=name, x_coord=x_pos, z_coord=z_pos)
location = loc_type.objects.create(owner=player, name=name, x_coord=x_pos, z_coord=z_pos)
return location.json
return location.json
@command("POST")
@ -87,13 +91,19 @@ def register(player_name, discord_uuid):
:param player_name: Minecraft in-game name
:param discord_uuid: Discord UUID if registering from Discord
:return: JSON representation of the new Player
:raise: PlayerInDBError
:help: Registers your Discord and Minecraft account with the the database
'''
mc_uuid = grab_UUID(player_name)
player = Player.objects.create(name=player_name, mc_uuid=mc_uuid, discord_uuid=discord_uuid)
return player.json
try:
get_player(mc_uuid=mc_uuid)
raise PlayerInDBError
except Player.DoesNotExist:
player = Player.objects.create(name=player_name, mc_uuid=mc_uuid, discord_uuid=discord_uuid)
player.save()
return player.json
@command("POST")
@ -106,6 +116,7 @@ def add_base(x_pos, z_pos, name=None, discord_uuid=None, mc_uuid=None):
:param discord_uuid: Discord UUID
:param mc_uuid: Minecraft UUID
:return: JSON representation of the new base
:raises: EntryNameNotUniqueError, PlayerNotFound, LocationLookupError
:help: Adds your base to the database. The base name is optional if this is your first base
'''
@ -122,6 +133,7 @@ def add_shop(x_pos, z_pos, name=None, discord_uuid=None, mc_uuid=None):
:param discord_uuid: Discord UUID
:param mc_uuid: Minecraft UUID
:return: JSON representation of the new shop
:raises: EntryNameNotUniqueError, PlayerNotFound, LocationLookupError
:help: Adds your shop to the database. The name is optional if this is your first shop
'''
@ -138,12 +150,23 @@ def add_tunnel(tunnel_direction, tunnel_number, location_name=None, discord_uuid
:param discord_uuid: Discord UUID
:param mc_uuid: Minecraft UUID
:return: JSON Representation of the Tunnel
:raises: PlayerNotFound, LocationHasTunnelError, EntryNameNotUniqueError, NoLocationsInDatabase
:help: Adds your tunnel to the database. If you only have one location, you do not need to specify a location name
'''
player = get_player(discord_uuid, mc_uuid)
if location_name is None:
loc = get_location(player, name=location_name)
loc = get_location(player)
location_name = loc.name
else:
loc = get_location(player, name=location_name)
for direction in Tunnel.TUNNEL_NAMES:
if re.search("{}.*".format(tunnel_direction), direction[1], re.IGNORECASE):
tunnel_direction = direction[0]
break
if Tunnel.objects.filter(location=loc).first():
raise LocationHasTunnelError
tunnel = Tunnel.objects.create(tunnel_direction=tunnel_direction, tunnel_number=tunnel_number,
location=get_location(player, location_name))
@ -152,14 +175,15 @@ def add_tunnel(tunnel_direction, tunnel_number, location_name=None, discord_uuid
@command("GET")
def find_location(search):
def find_location(search, limit=25):
'''
:request: GET
:param search: Location name or owner to search for
:param limit: How man locations to return
:return: List of the matching locations
:raises: LocationLookupError
:help: Finds all the locations matching the search term
'''
limit = 25
locations = Location.objects.filter(Q(name__icontains=search) | Q(owner__name__icontains=search)).all()[:limit]
@ -177,12 +201,17 @@ def delete(name, discord_uuid=None, mc_uuid=None):
:param discord_uuid: Discord UUID
:param mc_uuid: Minecraft UUID
:return: Location Name
:raises: LocationLookUpError
:help: Deletes a location from the database
'''
owner = get_player(discord_uuid, mc_uuid)
Location.objects.get(name__iexact=name, owner=owner).delete()
return name
owner = get_player(discord_uuid, mc_uuid)
try:
Location.objects.get(name__iexact=name, owner=owner).delete()
except Location.DoesNotExist:
raise LocationLookUpError
return name
@command("GET")
def find_around(x_pos, z_pos, radius=200):
@ -192,12 +221,16 @@ def find_around(x_pos, z_pos, radius=200):
:param z_pos: MC Z Coordinate
:param radius: Radius to each around (default 200)
:return: List of all locations in the radius
:raises: LocationLookupError
:help: Finds all the locations around a certain point
'''
locations = Location.objects.filter(x_coord__range=(x_pos - radius, x_pos + radius),
z_coord__range=(z_pos - radius, z_pos + radius)).all()
if len(locations) == 0:
raise LocationLookUpError
return objects_list_to_json(locations)
@ -212,6 +245,7 @@ def add_item(item_name, quantity, diamond_price, shop_name=None, discord_uuid=No
:param discord_uuid: Discord UUID
:param mc_uuid: Minecraft UUID
:return: Item Listing
:raises: PlayerNotFound, LocationLookupError, EntryNameNotUniqueError, NoLocationsInDatabase
:help: Adds an item to a shop's inventory. If you have one shop, the shop name is not required
'''
@ -219,18 +253,20 @@ def add_item(item_name, quantity, diamond_price, shop_name=None, discord_uuid=No
shop = get_location(player, shop_name, Shop).shop
item_listing = ItemListing.objects.create(shop=shop, amount=quantity, price=diamond_price, item_name=item_name)
item_listing = ItemListing.objects.create(shop=shop, amount=int(quantity), price=int(diamond_price),
item_name=item_name)
return item_listing.json
@command("GET")
def selling(item_name, sort="normalized_price"):
def selling(item_name, sort="-date_restocked"):
'''
:request: GET
:param item_name: Item name to search for
:param sort: Field to sort shop results by, default is normalized_price
:param sort: Field to sort shop results by, default is date_restocked
:return: List of top matching shops, sorted by the
:raises: ItemNotFound
:help: Lists shops selling an item.
'''
@ -271,7 +307,7 @@ def info(location_name):
:request: GET
:param location_name: Name of the location to get info on
:return: JSON representation of location
:raise LocationLookupError: If no location is found
:raises: LocationLookupError
:help: Finds all the locations matching the search term
'''
@ -292,6 +328,8 @@ def tunnel(player_name):
:request: GET
:param player_name: MC player name
:return: List of all the tunnels a user owns
:raises: LocationLookUpError
:raises: LocationLookupError
:help: Finds all the tunnels a player owns
'''
@ -312,7 +350,9 @@ def edit_pos(x, z, loc_name, discord_uuid=None, mc_uuid=None):
:param loc_name: Location Name to edit
:param discord_uuid: Discord UUID
:param mc_uuid: MC UUID
:return: Edited Location
:return: Edited Locatio
:raises:
:raises: PlayerNotFound, LocationLookupError, EntryNameNotUniqueError, NoLocationsInDatabase
:help: Edits the position of a location
'''
@ -336,6 +376,7 @@ def edit_tunnel(tunnel_direction, tunnel_number, loc_name, discord_uuid=None, mc
:param discord_uuid: Discord UUID
:param mc_uuid: Minecraft UUID
:return: Edited Location
:raises: PlayerNotFound, LocationLookupError, EntryNameNotUniqueError, NoLocationsInDatabase
:help: Edits the tunnel of a location
'''
@ -360,6 +401,7 @@ def edit_name(new_name, loc_name, discord_uuid=None, mc_uuid=None):
:param discord_uuid: Discord UUID
:param mc_uuid: MC UUID
:return: Edited Location
:raises: PlayerNotFound, LocationLookupError, EntryNameNotUniqueError, NoLocationsInDatabase
:help: Edits the name of a location
'''
@ -381,6 +423,8 @@ def delete_item(item, shop_name=None, discord_uuid=None, mc_uuid=None):
:param discord_uuid: Discord UUID
:param mc_uuid: Minecraft UUID
:return: Deletes an item from the database
:raises: PlayerNotFound, LocationLookupError, EntryNameNotUniqueError, NoLocationsInDatabase
:help: Deletes an item from a shop
'''
player = get_player(discord_uuid=discord_uuid, mc_uuid=mc_uuid)
@ -399,15 +443,19 @@ def me(discord_uuid=None, mc_uuid=None):
:param discord_uuid: Discord UUID
:param mc_uuid: MC UUID
:return: Returns a list of all the locations owned by a user
:raises: NoLocationsInDatabase, PlayerNotFound
:help: Find all the locations in the database
'''
player = get_player(discord_uuid=discord_uuid, mc_uuid=mc_uuid)
try:
player = get_player(discord_uuid=discord_uuid, mc_uuid=mc_uuid)
except Player.DoesNotExist:
raise PlayerNotFound
locations = Location.objects.filter(owner=player).all()
if len(locations) == 0:
raise PlayerNotFound
raise NoLocationsInDatabase
return objects_list_to_json(locations)
@ -421,6 +469,7 @@ def restock(item_name, shop_name=None, discord_uuid=None, mc_uuid=None):
:param discord_uuid: Discord UUID
:param mc_uuid: Minecraft UUID
:return: List of items updated
:raises: PlayerNotFound, LocationLookupError, EntryNameNotUniqueError, NoLocationsInDatabase
:help: Restocks items matching the item name in your shop
'''
owner = get_player(discord_uuid, mc_uuid)

View File

@ -13,3 +13,4 @@ goods.
getting_started
commands
models

View File

@ -0,0 +1,27 @@
.. _models:
Models
======
Models used in Geoffrey
.. autoclass:: GeoffreyApp.models.APIToken
:members:
.. autoclass:: GeoffreyApp.models.Player
:members:
.. autoclass:: GeoffreyApp.models.Location
:members:
.. autoclass:: GeoffreyApp.models.Shop
:members:
.. autoclass:: GeoffreyApp.models.Base
:members:
.. autoclass:: GeoffreyApp.models.ItemListing
:members:
.. autoclass:: GeoffreyApp.models.Tunnel
:members:

View File

@ -9,8 +9,19 @@ from GeoffreyApp.util import create_token
class APIToken(models.Model):
key = models.CharField(default=create_token, max_length=25, unique=True)
'''
Key used to access the Geoffrey API
'''
name = models.CharField(max_length=50, blank=True)
'''
Name of the key
'''
commands_perm = models.BooleanField(default=False)
'''
Permission to use the command api
'''
def __str__(self):
if len(self.name):
@ -21,8 +32,19 @@ class APIToken(models.Model):
class Player(models.Model):
name = models.CharField(max_length=30, unique=True)
'''
Player username
'''
mc_uuid = models.CharField(max_length=36, unique=True)
'''
Minecraft UUID
'''
discord_uuid = models.CharField(max_length=50, unique=True)
'''
Discord UUID
'''
@property
def json(self):
@ -43,12 +65,26 @@ class Location(models.Model):
)
name = models.CharField(max_length=128, unique=True)
'''
Name of the location
'''
x_coord = models.IntegerField()
'''
X Position
'''
z_coord = models.IntegerField()
'''
Z Position
'''
dimension = models.CharField(max_length=1, choices=DIMENSIONS)
owner = models.ForeignKey(Player, related_name='owner_player', on_delete=models.CASCADE)
'''
Owner of Location
'''
@property
def json(self):
@ -76,10 +112,29 @@ class Base(Location):
class ItemListing(models.Model):
item_name = models.CharField(max_length=128)
'''
Name of the item
'''
price = models.IntegerField()
'''
Number of diamonds per amount of items
'''
amount = models.IntegerField()
'''
Number of items
'''
date_restocked = models.DateTimeField(auto_now=True)
'''
Datetime the item was last restocked
'''
shop = models.ForeignKey(Shop, related_name="shop_selling", on_delete=models.CASCADE)
'''
Shop the item is sold at
'''
@property
def normalized_price(self):
@ -93,26 +148,41 @@ class ItemListing(models.Model):
return {"item_name": self.item_name,
"price": self.price,
"amount": self.amount,
"date_restocked": self.date_restocked,
"normalized_price": self.normalized_price,
"shop": self.shop.json,
}
def __str__(self):
return "Item: %d %s for %d" % (self.amount, self.item_name, self.amount)
class Tunnel(models.Model):
TUNNEL_NAMES = (
('N', getattr(settings, 'NORTH', '')),
('E', getattr(settings, 'EAST', '')),
('S', getattr(settings, 'SOUTH', '')),
('W', getattr(settings, 'WEST', ''))
('N', getattr(settings, 'GEOFFREY_NORTH_TUNNEL', '')),
('E', getattr(settings, 'GEOFFREY_EAST_TUNNEL', '')),
('S', getattr(settings, 'GEOFFREY_SOUTH_TUNNEL', '')),
('W', getattr(settings, 'GEOFFREY_WEST_TUNNEL', ''))
)
'''
Tunnel Direction
'''
tunnel_number = models.IntegerField()
'''
Tunnel coordinate
'''
tunnel_direction = models.CharField(max_length=1, choices=TUNNEL_NAMES)
'''
Tunnel Direction
'''
location = models.ForeignKey(Location, related_name="tunnel_location", on_delete=models.CASCADE)
'''
Location that the tunnel is for
'''
def __str__(self):
return "Tunnel: %s %d" % (self.tunnel_direction, self.tunnel_number)

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +0,0 @@
.sidebar-nav {
list-style-type: none;
margin: 0;
padding: 0;
width: 200px;
background-color: #f1f1f1;
}
.sidebar-option{
float: left;
}
.sidebar-option .sidebar-a {
display: block;
color: #000;
padding: 8px 16px;
text-decoration: none;
}
/* Change the link color on hover */
.sidebar-option .sidebar-a:hover {
background-color: #555;
color: white;
}

View File

@ -1,44 +1,47 @@
<!DOCTYPE html>
{% load static %}
{% load navbar %}
<html lang="en">
<head>
<link rel="shortcut icon" type="image/png" href="{% static 'GeoffreyApp/img/icon.png' %}"/>
<meta charset="UTF-8">
{% block title %}<title>GeoffreyApp</title>{% endblock %}
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<!-- Add additional CSS in static file -->
<!-- jQuery library -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!-- Popper JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"></script>
<!-- Latest compiled JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'GeoffreyApp/css/style.css' %}">
<link rel="stylesheet" href="{% static 'GeoffreyApp/css/bootstrap.css' %}">
</head>
<body>
<body>
<nav class="navbar">
<div class="container-fluid">
<div class="navbar-header">
<a<img src="{% static 'GeoffreyApp/img/icon.png' %}" alt="Sed" height="100" width="100"/>
<a class="navbar-brand" style="color:inherit;cursor:default;">Geoffrey</a>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-sm-2">
{% block sidebar %}
<ul class="sidebar-nav">
<li class="sidebar-option"><a class="sidebar-a" href="{% url 'GeoffreyHome' %}">Home</a></li>
<li class="sidebar-option"><a class="sidebar-a" href="{% url 'GeoffreyPlayers' %}">Players</a></li>
<li class="sidebar-option"><a class="sidebar-a" href="{% url 'GeoffreyBases' %}">Bases</a></li>
<li class="sidebar-option"><a class="sidebar-a" href="{% url 'GeoffreyShops' %}">Shops</a></li>
<li class="sidebar-option"><a class="sidebar-a" href="{% url 'GeoffreyItems' %}">Items</a></li>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a href="#" class="navbar-left"><img src="{% static 'GeoffreyApp/img/icon.png' %}" height="30px" alt="sed"></a>
<a class="navbar-brand" href="#">Geoffrey</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbar">
<ul class="navbar-nav mr-auto">
{% autoescape off %}{% get_navbar current_page|safe %}{% endautoescape %}
</ul>
{% endblock %}
</div>
<div class="col-sm-10 ">{% block content %}{% endblock %}</div>
<form class="form-inline my-2 my-lg-0" id="searchform" action="{% url 'GeoffreySearch' %}">
<input class="form-control mr-sm-2" name="search" type="text" placeholder="Search">
<button class="btn btn-secondary my-2 my-sm-0" type="submit">Search</button>
{% block search %}{% endblock %}
</form>
</div>
</div>
</nav>
<div class="container-fluid">
{% block content %}{% endblock %}
</div>
</body>
</html>

View File

@ -1,7 +1,7 @@
{% extends "GeoffreyApp/base.html" %}
{% block content %}
<h1>Geoffrey Minecraft Database Home</h1>
<h2>Geoffrey Minecraft Database Home</h2>
<p>Geoffrey is a database for storing information on Players, Bases, Shops, Towns, and more! </p>
<p>Current Database Count:</p>
<ul>

View File

@ -1,7 +1,6 @@
{% extends "GeoffreyApp/base.html" %}
{% block content %}
<h1>Items</h1>
{% if itemlisting_list %}
<ul>
{% for item in itemlisting_list %}

View File

@ -1,7 +1,7 @@
{% extends "GeoffreyApp/base.html" %}
{% block content %}
<h1>Players</h1>
{% if player_list %}
<ul>
{% for player in player_list %}

View File

@ -0,0 +1,42 @@
{% extends "GeoffreyApp/base.html" %}
{% block content %}
<h4>Search results for {{ search }}...</h4>
{% if player_list %}
<h6>Players</h6>
<ul>
{% for player in player_list %}
<li>
{{ player.name }}
</li>
{% endfor %}
</ul>
{% else %}
{% endif %}
{% if base_list %}
<h6>Bases</h6>
<ul>
{% for base in base_list %}
<li>
{{ base.name }}
</li>
{% endfor %}
</ul>
{% else %}
{% endif %}
{% if shop_list %}
<h6>Shops</h6>
<ul>
{% for shop in shop_list %}
<li>
{{ shop.name }}
</li>
{% endfor %}
</ul>
{% else %}
{% endif %}
{% endblock %}

View File

@ -3,12 +3,6 @@
{% block content %}
<h1>Shops</h1>
<form id="searchform" action="{% url 'GeoffreyShops' %}" method="get" accept-charset="utf-8">
<button class="searchbutton" type="submit">
<i class="fa fa-search">Go</i>
</button>
<input class="searchfield" id="searchbox" name="q" type="text" placeholder="Search">
</form>
{% if shop_list %}
<ul>
{% for shop in shop_list %}

View File

View File

@ -0,0 +1,29 @@
from django import template
from django.urls import reverse
register = template.Library()
navbar_options = [
("Home", reverse("GeoffreyHome")),
("Players", reverse("GeoffreyPlayers")),
("Shops", reverse("GeoffreyShops")),
("Bases", reverse("GeoffreyBases")),
("Item Listings", reverse("GeoffreyItems"))
]
option_format = '<li class="nav-item{}"> <a class="nav-link" href="{}">{} </a> </li>'
@register.simple_tag
def get_navbar(page):
navbar = ""
for option in navbar_options:
if page == option[0]:
active = ' active'
else:
active = ''
navbar += option_format.format(active, option[1], option[0])
return navbar

View File

@ -39,6 +39,8 @@ class CommandsAPITestCase(TestCase):
self.assertEqual(count, 1)
self.assertRaises(PlayerInDBError, register, player_name="Vakky", discord_uuid="229423434256351233")
def test_add_base(self):
add_base(x_pos=0, z_pos=0, name=None, discord_uuid=DISCORD_UUID)
@ -46,6 +48,8 @@ class CommandsAPITestCase(TestCase):
self.assertEqual(base.owner.name, "ZeroHD")
self.assertRaises(EntryNameNotUniqueError, add_base, x_pos=0, z_pos=0, name=None, discord_uuid=DISCORD_UUID)
def test_add_shop(self):
add_shop(x_pos=0, z_pos=0, name=None, discord_uuid=DISCORD_UUID)

View File

@ -7,5 +7,6 @@ urlpatterns = [
url(r'^shops$', views.ShopList.as_view(), name='GeoffreyShops'),
url(r'^bases$', views.BaseList.as_view(), name='GeoffreyBases'),
url(r'^items$', views.ItemListingList.as_view(), name='GeoffreyItems'),
url(r'^search$', views.SearchList.as_view(), name='GeoffreySearch'),
]

View File

@ -20,18 +20,44 @@ class Home(View):
"num_bases": base_num,
"num_shops": shop_num,
"num_items": item_num,
"current_page": "Home",
}
return render(request, 'GeoffreyApp/home.html', context=context)
class SearchList(View):
def get(self, request):
context = {}
query = request.GET.get('search')
context["search"] = query
context["player_list"] = Player.objects.filter(Q(name__icontains=query)).all()
context["base_list"] = Base.objects.filter(Q(name__icontains=query) | Q(owner__name__icontains=query)).all()
context["shop_list"] = Shop.objects.filter(Q(name__icontains=query) | Q(owner__name__icontains=query)).all()
return render(request, 'GeoffreyApp/search.html', context=context)
class PlayerList(generic.ListView):
model = Player
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['current_page'] = "Players"
return context
class ShopList(generic.ListView):
model = Shop
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['current_page'] = "Shops"
return context
def get_queryset(self):
qs = Shop.objects.all()
@ -45,7 +71,16 @@ class ShopList(generic.ListView):
class BaseList(generic.ListView):
model = Base
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['current_page'] = "Bases"
return context
class ItemListingList(generic.ListView):
model = ItemListing
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['current_page'] = "Item Listings"
return context