diff --git a/bottle/.gitignore b/bottle/.gitignore new file mode 100644 index 0000000..46f885b --- /dev/null +++ b/bottle/.gitignore @@ -0,0 +1,12 @@ +.idea/ +__pycache__/ +bin/ +include/ +lib/ +lib64 +pip-selfcheck.json +*.pyc +kontor.properties +*.nja +dist/ +kontor_bottle.egg-info/ diff --git a/bottle/Jenkinsfile b/bottle/Jenkinsfile new file mode 100644 index 0000000..61b613c --- /dev/null +++ b/bottle/Jenkinsfile @@ -0,0 +1,13 @@ +node { + stage("Checkout") { + checkout scm + } + stage("setup virualenv") { + sh "virtualenv ." + sh "source bin/activate; pip install bottle pymongo" + } + stage("build") { + sh "source bin/activate; python setup.py sdist" + } +} + diff --git a/bottle/README.md b/bottle/README.md new file mode 100644 index 0000000..67c879f --- /dev/null +++ b/bottle/README.md @@ -0,0 +1,2 @@ +# Kontor Bottle + diff --git a/bottle/comics/__init__.py b/bottle/comics/__init__.py new file mode 100644 index 0000000..f8b4dc1 --- /dev/null +++ b/bottle/comics/__init__.py @@ -0,0 +1,196 @@ +# -*- coding:utf-8 -*- +import pymongo +import publisherDAO +import artistDAO +import comicDAO +import storyArcDAO +import bottle +import cgi + + +__author__ = 'tpeetz' + + +class Plugin: + + def __init__(self, app, database, sessions): + self.app = app + self.db = database + self.sessions = sessions + self.publishers = publisherDAO.PublisherDAO(database) + self.artists = artistDAO.ArtistDAO(database) + self.comics = comicDAO.ComicDAO(database) + self.storyarcs = storyArcDAO.StoryArcDAO(database) + self.routing() + + + def routing(self): + self.app.route('/comics', 'GET', self.comic_index) + self.app.route('/comics/', 'GET', self.comic_index) + self.app.route('/comics/comic', 'GET', self.comic_list) + self.app.route('/comics/comic/', 'GET', self.comic_details) + self.app.route('/comics/comic/create', 'GET', self.get_comic_create) + self.app.route('/comics/comic/create', 'POST', self.post_create_comic) + self.app.route('/comics/publisher', 'GET', self.publisher_list) + self.app.route('/comics/publisher/', 'GET', self.publisher_details) + self.app.route('/comics/publisher/create', 'GET', self.get_publisher_create) + self.app.route('/comics/publisher/create', 'POST', self.post_create_publisher) + self.app.route('/comics/artist', 'GET', self.artist_list) + self.app.route('/comics/artist/', 'GET', self.artist_details) + self.app.route('/comics/artist/create', 'GET', self.get_artist_create) + self.app.route('/comics/artist/create', 'POST', self.post_create_artist) + self.app.route('/comics/storyarc', 'GET', self.storyarc_list) + self.app.route('/comics/storyarc/', 'GET', self.storyarc_details) + self.app.route('/comics/storyarc/create', 'GET', self.get_storyarc_create) + self.app.route('/comics/storyarc/create', 'POST', self.post_create_storyarc) + + def comic_index(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + return bottle.template('comic_index', dict(username=username)) + + + def comic_list(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + l = self.comics.get_comics() + return bottle.template('comic_list', dict(comics=l, username=username)) + + + def comic_details(self, id): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + comic = self.comics.get_comic(id) + errors = "" + if comic == None: + errors = "Entry not found" + return bottle.template('comic_template', dict(title=comic['title'], + id=comic['_id'], + current_order=comic['current_order'], + completed=comic['completed'], + errors="", + username=username)) + + + def get_comic_create(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + return bottle.template("comic_template", dict(title="", + id='newentry', + current_order=False, + completed=False, + errors="", + username=username)) + + + def post_create_comic(self): + comic_id = bottle.request.forms.get("id") + comic_title = bottle.request.forms.get("title") + comic_order = bottle.request.forms.get("current_order") + comic_completed = bottle.request.forms.get("completed") + if comic_id == "newentry": + self.comics.insert_entry(comic_title, None, comic_order, comic_completed) + else: + self.comics.update_entry(comic_id, comic_title, None, comic_order, comic_completed) + bottle.redirect("/comics/comic") + + + def publisher_list(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + l = self.publishers.get_publishers() + return bottle.template('publisher_list', dict(publishers=l, username=username)) + + + def publisher_details(self, id): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + publisher = self.publishers.get_publisher(id) + errors = "" + if publisher == None: + errors= "Entry not found" + return bottle.template('publisher_template', dict(name=publisher['name'], id=publisher['_id'], errors="", username=username)) + + + def get_publisher_create(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + return bottle.template("publisher_template", dict(name="", id='newentry', errors="", username=username)) + + + def post_create_publisher(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + publisher_id = bottle.request.forms.get("id") + publisher_name = bottle.request.forms.get("name") + if publisher_id == "newentry": + self.publishers.insert_entry(publisher_name) + else: + self.publishers.update_entry(publisher_id, publisher_name) + bottle.redirect("/comics/publisher") + + + def artist_list(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + l = self.artists.get_artists() + return bottle.template('artist_list', dict(artists=l, username=username)) + + + def artist_details(self, id): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + artist = self.artists.get_artist(id) + errors = "" + if artist == None: + errors= "Entry not found" + return bottle.template('artist_template', dict(name=artist['name'], id=artist['_id'], errors="", username=username)) + + + def get_artist_create(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + return bottle.template("artist_template", dict(name="", id='newentry', errors="", username=username)) + + + def post_create_artist(self): + artist_id = bottle.request.forms.get("id") + artist_name = bottle.request.forms.get("name") + if artist_id == "newentry": + self.artists.insert_entry(artist_name) + else: + self.artists.update_entry(artist_id, artist_name) + bottle.redirect("/comics/artist") + + + def storyarc_list(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + l = self.storyarcs.get_storyarcs() + return bottle.template('storyarc_list', dict(storyarcs=l, username=username)) + + + def storyarc_details(self, id): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + storyarc = self.storyarcs.get_storyarc(id) + errors = "" + if storyarc == None: + errors = "Entry not found" + return bottle.template('storyarc_template', dict(title=storyarc['title'], id=storyarc['_id'], errors="", username=username)) + + + def get_storyarc_create(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + return bottle.template("storyarc_template", dict(title="", id='newentry', errors="", username=username)) + + + def post_create_storyarc(self): + storyarc_id = bottle.request.forms.get("id") + storyarc_title = bottle.request.forms.get("title") + if storyarc_id == "newentry": + self.storyarcs.insert_entry(storyarc_title) + else: + self.storyarcs.update_entry(storyarc_id, storyarc_title) + bottle.redirect("/comics/storyarc") diff --git a/bottle/comics/artistDAO.py b/bottle/comics/artistDAO.py new file mode 100644 index 0000000..b2a6003 --- /dev/null +++ b/bottle/comics/artistDAO.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from bson.objectid import ObjectId +import sys + +class ArtistDAO: + + # constructor for the class + def __init__(self, database): + self.db = database + self.artists = database.artists + + def get_artists(self): + cursor = self.artists.find() + l = [] + for artist in cursor: + l.append({'name':artist['name'], '_id':artist['_id']}) + return l + + def get_artist(self, artist_id): + artist = self.artists.find_one({"_id": ObjectId(artist_id)}) + return artist + + def insert_entry(self, artist_name): + print "inserting artist entry", artist_name + + artist = {"name": artist_name} + + # now insert the post + try: + result = self.artists.insert_one(artist) + print "Matching artist: ", result.matched_count + print "Modified artist: ", result.modified_count + except: + print "Error inserting artist" + print "Unexpected error:", sys.exc_info() + + def update_entry(self, artist_id, artist_name): + print "upserting artist entry", artist_name + + filter_doc = {"_id": ObjectId(artist_id)} + artist = { "$set": {"name": artist_name}} + + # now insert the post + try: + result = self.artists.update_one(filter_doc, artist) + print "Modified artist: ", result.modified_count + except: + print "Error inserting artist" + print "Unexpected error:", sys.exc_info() + diff --git a/bottle/comics/comicDAO.py b/bottle/comics/comicDAO.py new file mode 100644 index 0000000..6caa8ce --- /dev/null +++ b/bottle/comics/comicDAO.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +from bson.objectid import ObjectId +import sys + +class ComicDAO: + + # constructor for the class + def __init__(self, database): + self.db = database + self.comics = database.comics + + def get_comics(self): + cursor = self.comics.find() + l = [] + for comic in cursor: + l.append({'title':comic['title'], + '_id':comic['_id'], + 'current_order':comic['current_order'], + 'completed':comic['completed']}) + return l + + def get_comic(self, comic_id): + comic = self.comics.find_one({"_id": ObjectId(comic_id)}) + return comic + + def insert_entry(self, comic_title, comic_publisher=None, comic_order=False, comic_completed=False): + print "inserting comic entry", comic_title + + comic = {"title": comic_title, "current_order": comic_order, "completed": comic_completed} + + # now insert the comic + try: + result = self.comics.insert_one(comic) + print "Modified comic: ", result.modified_count + except: + print "Error inserting comic" + print "Unexpected error:", sys.exc_info() + + def update_entry(self, comic_id, comic_title, comic_publisher=None, comic_order=False, comic_completed=False): + print "upserting comic entry", comic_title + + filter_doc = {"_id": ObjectId(comic_id)} + comic = { "$set": {"title": comic_title, 'current_order': comic_order, 'completed': comic_completed}} + + # now insert the post + try: + result = self.comics.update_one(filter_doc, comic) + print "Matching comic: ", result.matched_count + print "Modified comic: ", result.modified_count + except: + print "Error inserting comic" + print "Unexpected error:", sys.exc_info() + diff --git a/bottle/comics/models.py b/bottle/comics/models.py new file mode 100644 index 0000000..1cca3b1 --- /dev/null +++ b/bottle/comics/models.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +from mongoengine import connect, Document +from mongoengine import StringField, BooleanField +from mongoengine import ListField, ReferenceField, GenericReferenceField +import pprint + + +class Publisher(Document): + name = StringField(required=True) + + def __str__(self): + s = "Publisher(%s)" % self.name + return s + + +class Artist(Document): + name = StringField(required=True) + className = StringField() + + def __str__(self): + s = "Artist(%s)" % self.name + return s + + +class Issue(Document): + number = StringField() + comic = GenericReferenceField() + is_read = BooleanField(default=False) + is_stock = BooleanField(default=False) + + def __str__(self): + s = "Issue(%s # %s, %s)" % (self.comic.title, self.number, self.is_read) + return s + + +class StoryArc(Document): + name = StringField(required=True) + comic = GenericReferenceField() + issues = ListField(ReferenceField(Issue)) + + def __str__(self): + s = "StoryArc(%s, %s)" % (self.name, self.comic.title) + return s + + +class Volume(Document): + name = StringField(required=True) + comic = GenericReferenceField() + issues = ListField(ReferenceField(Issue)) + + def __str__(self): + s = "Volume(%s, %s, %s)" % (self.id, self.name, self.comic.title) + return s + + +class Comic(Document): + title = StringField(required=True) + publisher = ReferenceField(Publisher) + current_order = BooleanField() + completed = BooleanField() + issues = ListField(ReferenceField(Issue)) + stories = ListField(ReferenceField(StoryArc)) + + def __str__(self): + if self.publisher is None: + s = "Comic(%s, %s, %s, %s)" % (self.title, self.publisher, self.current_order, self.completed) + return s + else: + s = "Comic(%s, %s, %s, %s)" % ( + self.title, self.publisher.name, self.current_order, self.completed) + return s + + +class TradePaperback(Document): + comic = ReferenceField(Comic) + issue_start = StringField() + issue_end = StringField() + + def __str__(self): + s = "TPB(%s)" % self.comic.title + return s + + +def get_publisher(name): + publisher = Publisher.objects(name=name) + if publisher.count() > 0: + return publisher[0] + else: + return None + + +def get_comic(title): + comic = Comic.objects(title=title) + if comic.count() > 0: + return comic[0] + else: + return None + + +def get_issue(title, number): + comic = get_comic(title) + issues = Issue.objects(number=number, comic=comic) + if issues.count() > 0: + return issues[0] + else: + return None + +if __name__ == '__main__': + connect('comics') + for publisher in Publisher.objects: + pprint.pprint(publisher) + for artist in Artist.objects: + pprint.pprint(artist) + for comic in Comic.objects: + pprint.pprint(comic) + for issue in Issue.objects: + pprint.pprint(issue) + for story in StoryArc.objects: + pprint.pprint(story) + for tpb in TradePaperback.objects: + pprint.pprint(tpb) diff --git a/bottle/comics/publisherDAO.py b/bottle/comics/publisherDAO.py new file mode 100644 index 0000000..aeb3ad8 --- /dev/null +++ b/bottle/comics/publisherDAO.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from bson.objectid import ObjectId +import sys + +class PublisherDAO: + + # constructor for the class + def __init__(self, database): + self.db = database + self.publishers = database.publishers + + def get_publishers(self): + cursor = self.publishers.find() + l = [] + for publisher in cursor: + l.append({'name':publisher['name'], '_id':publisher['_id']}) + return l + + def get_publisher(self, publisher_id): + publisher = self.publishers.find_one({"_id": ObjectId(publisher_id)}) + return publisher + + def insert_entry(self, publisher_name): + print "inserting publisher entry", publisher_name + + publisher = {"name": publisher_name} + + # now insert the post + try: + result = self.publishers.insert_one(publisher) + print "Matching publisher: ", result.matched_count + print "Modified publisher: ", result.modified_count + except: + print "Error inserting publisher" + print "Unexpected error:", sys.exc_info() + + def update_entry(self, publisher_id, publisher_name): + print "upserting publisher entry", publisher_name + + filter_doc = {"_id": ObjectId(publisher_id)} + publisher = { "$set": {"name": publisher_name}} + + # now insert the post + try: + result = self.publishers.update_one(filter_doc, publisher) + print "Modified publisher: ", result.modified_count + except: + print "Error inserting publisher" + print "Unexpected error:", sys.exc_info() + diff --git a/bottle/comics/storyArcDAO.py b/bottle/comics/storyArcDAO.py new file mode 100644 index 0000000..d987fc8 --- /dev/null +++ b/bottle/comics/storyArcDAO.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +from bson.objectid import ObjectId +import sys + + +class StoryArcDAO: + # constructor for the class + def __init__(self, database): + self.db = database + self.storyarcs = database.storyarcs + + def get_storyarcs(self): + cursor = self.storyarcs.find() + l = [] + for storyarc in cursor: + l.append({'title': storyarc['title'], '_id': storyarc['_id']}) + return l + + def get_storyarc(self, storyarc_id): + storyarc = self.storyarcs.find_one({"_id": ObjectId(storyarc_id)}) + return storyarc + + def insert_entry(self, storyarc_title): + print "inserting publisher entry", storyarc_title + + storyarc = {"name": storyarc_title} + + # now insert the post + try: + result = self.storyarcs.insert_one(storyarc) + print "Matching storyarc: ", result.matched_count + print "Modified storyarc: ", result.modified_count + except: + print "Error inserting storyarc" + print "Unexpected error:", sys.exc_info() + + def update_entry(self, storyarc_id, storyarc_title): + print "upserting storyarc entry", storyarc_title + + filter_doc = {"_id": ObjectId(storyarc_id)} + storyarc = {"$set": {"name": storyarc_title}} + + # now insert the post + try: + result = self.storyarcs.update_one(filter_doc, storyarc) + print "Modified storyarc: ", result.modified_count + except: + print "Error inserting storyarc" + print "Unexpected error:", sys.exc_info() diff --git a/bottle/homeoffice/__init__.py b/bottle/homeoffice/__init__.py new file mode 100644 index 0000000..3c7ba41 --- /dev/null +++ b/bottle/homeoffice/__init__.py @@ -0,0 +1,28 @@ +# -*- coding:utf-8 -*- +import pymongo +import bottle +import cgi + + +__author__ = 'tpeetz' + + +class Plugin: + + def __init__(self, app, database, sessions): + self.app = app + self.db = database + self.sessions = sessions + self.routing() + + + def routing(self): + self.app.route('/office', 'GET', self.office_index) + self.app.route('/office/travel', 'GET', self.office_index) + + + def office_index(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + return bottle.template('office_index', dict(username=username)) + diff --git a/bottle/kontor.css b/bottle/kontor.css new file mode 100644 index 0000000..c61938e --- /dev/null +++ b/bottle/kontor.css @@ -0,0 +1,109 @@ +body { + font-family: sans-serif; + color: #333333; + padding: 3em 0 4em; +} + +body, +.wrapper { + margin: 10px auto; + /*max-width: 60em;*/ +} + +header, main, section, footer { + border-radius: 0px 0.5em 0.5em; + border: 1px solid; + padding: 10px; + margin: 10px; +} + +header { + position:fixed; + top:0px; + left:0px; + right:0px; + text-align:center; + padding: 10px; + background: lightgrey; + /*border-bottom: 1px solid #d5d5d5;*/ +} + +nav { + /*position: fixed;*/ + /*padding-top: 10em; */ + + border-radius: 0px 0.5em 0.5em; + border: 1px solid; + + padding: 0; + margin: 10px; + + font-size: 0.91em; + float: left; + width: 15em; + background: lightskyblue; + border-color: skyblue; +} + +nav ul { + padding: 0; +} + +nav li { + list-style: none; + margin: 0.4em; + padding: 0; +} + +nav ul ul { + margin: 0 0 0 2em; + padding: 0; + border: none; +} + +nav ul ul li { + margin: 0.3em 0; +} + +nav a { + display: block; + padding: 0.4em; + text-decoration: none; + font-weight: bold; + border: 1px solid blue; + border-radius: 10px; + box-shadow: 0px 5px 10px white inset; + background-color: skyblue; + color: #333; + +} + +nav a:focus, +nav a:hover { + color: royalblue; + background-color: gold; +} + +main { + display: block; + background: lightblue; + border-color: #8a9da8; + margin-left: 15em; + min-width: 16em; /* Mindestbreite (der Überschrift) verhindert Anzeigefehler in modernen Browsern */ +} + +footer { + position:fixed; + padding: 10px; + margin-top: 10px; + bottom:0; + left: 0; + right:0; + background: lightgrey; + border-color: grey; +} + +footer p { + float:right; + margin: 0; +} diff --git a/bottle/kontor.py b/bottle/kontor.py new file mode 100644 index 0000000..d11e27f --- /dev/null +++ b/bottle/kontor.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +import pymongo +import sessionDAO +import userDAO +import homeoffice +import comics +import library +import medien +import tradingcards +import ConfigParser +import bottle +import cgi +import re + + +__author__ = 'tpeetz' + +app = bottle.Bottle() + + +def index(): + cookie = bottle.request.get_cookie("session") + username = sessions.get_username(cookie) + return bottle.template('kontor', dict(username=username)) + + +def show_signup(): + return bottle.template("signup", dict(username="", + password="", + password_error="", + email="", + username_error="", + email_error="", + verify_error ="")) + + +def process_signup(): + email = bottle.request.forms.get("email") + username = bottle.request.forms.get("username") + password = bottle.request.forms.get("password") + verify = bottle.request.forms.get("verify") + + # set these up in case we have an error case + errors = {'username': cgi.escape(username), 'email': cgi.escape(email)} + if validate_signup(username, password, verify, email, errors): + + if not users.add_user(username, password, email): + # this was a duplicate + errors['username_error'] = "Username already in use. Please choose another" + return bottle.template("signup", errors) + + session_id = sessions.start_session(username) + print session_id + bottle.response.set_cookie("session", session_id) + bottle.redirect("/welcome") + else: + print "user did not validate" + return bottle.template("signup", errors) + + +def show_login(): + return bottle.template('login', dict(username="", password="", login_error="")) + +def process_login(): + username = bottle.request.forms.get("username") + password = bottle.request.forms.get("password") + + print "user submitted ", username, "pass ", password + + user_record = users.validate_login(username, password) + if user_record: + # username is stored in the user collection in the _id key + session_id = sessions.start_session(user_record['_id']) + + if session_id is None: + bottle.redirect("/internal_error") + + cookie = session_id + + # Warning, if you are running into a problem whereby the cookie being set here is + # not getting set on the redirect, you are probably using the experimental version of bottle (.12). + # revert to .11 to solve the problem. + bottle.response.set_cookie("session", cookie) + + bottle.redirect("/") + + else: + return bottle.template("login", dict(username=cgi.escape(username), password="", login_error="Invalid Login")) + + +def process_logout(): + cookie = bottle.request.get_cookie("session") + sessions.end_session(cookie) + bottle.response.set_cookie("session", "") + bottle.redirect("/") + + +def send_stylesheet(filename): + return bottle.static_file(filename, root='.', mimetype='text/css') + + +def setup_routing(app): + app.route('/', 'GET', index) + app.route('/signup', 'GET', show_signup) + app.route('/signup', 'POST', process_signup) + app.route('/login', 'GET', show_login) + app.route('/login', 'POST', process_login) + app.route('/logout', 'GET', process_logout) + app.route('/css/', 'GET', send_stylesheet) + + +# validates that the user information is valid for new signup, return True of False +# and fills in the error string if there is an issue +def validate_signup(username, password, verify, email, errors): + USER_RE = re.compile(r"^[a-zA-Z0-9_-]{3,20}$") + PASS_RE = re.compile(r"^.{3,20}$") + EMAIL_RE = re.compile(r"^[\S]+@[\S]+\.[\S]+$") + + errors['username_error'] = "" + errors['password_error'] = "" + errors['verify_error'] = "" + errors['email_error'] = "" + + if not USER_RE.match(username): + errors['username_error'] = "invalid username. try just letters and numbers" + return False + + if not PASS_RE.match(password): + errors['password_error'] = "invalid password." + return False + if password != verify: + errors['verify_error'] = "password must match" + return False + if email != "": + if not EMAIL_RE.match(email): + errors['email_error'] = "invalid email address" + return False + return True + + +setup_routing(app) + +config = ConfigParser.ConfigParser() +config.read('kontor.properties') +server = config.get('host', 'server') +port = config.get('host', 'port') + +db_server = config.get('database', 'server') +db_port = config.get('database', 'port') +db_admin = config.get('database', 'adminDB') +db_user = config.get('database', 'user') +db_password = config.get('database', 'password') +connection_string = "mongodb://" +if db_admin: + connection_string += db_user + connection_string += ':' + connection_string += db_password + connection_string += '@' + connection_string += db_server + connection_string += '?/authsource=' + connection_string += db_admin +else: + connection_string += db_server +connection = pymongo.MongoClient(connection_string) +database = connection.kontor + +users = userDAO.UserDAO(database) +sessions = sessionDAO.SessionDAO(database) + +office_plugin = homeoffice.Plugin(app, database, sessions) +comics_plugin = comics.Plugin(app, database, sessions) +library_plugin = library.Plugin(app, database, sessions) +medien_plugin = medien.Plugin(app, database, sessions) +tradingcards_plugin = tradingcards.Plugin(app, database, sessions) + +bottle.run(app, host=server, port=port, debug=True, reloader=True) + diff --git a/bottle/library/__init__.py b/bottle/library/__init__.py new file mode 100644 index 0000000..10b5da7 --- /dev/null +++ b/bottle/library/__init__.py @@ -0,0 +1,66 @@ +# -*- coding:utf-8 -*- +import pymongo +import bookDAO +import bottle +import cgi + + +__author__ = 'tpeetz' + + +class Plugin: + + def __init__(self, app, database, sessions): + self.app = app + self.db = database + self.sessions = sessions + self.books = bookDAO.BookDAO(database) + self.routing() + + def routing(self): + self.app.route('/library', 'GET', self.book_list) + self.app.route('/library/book', 'GET', self.book_list) + self.app.route('/library/book/', 'GET', self.book_details) + self.app.route('/library/book/create', 'GET', self.get_book_create) + self.app.route('/library/book/create', 'POST', self.post_create_book) + + def book_list(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + l = self.books.get_books() + return bottle.template('book_list', dict(books=l, username=username)) + + def book_details(self, id): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + book = self.books.get_book(id) + errors = "" + if book == None: + errors = "Entry not found" + return bottle.template('book_template', dict(title=book['title'], + id=book['_id'], + current_order=book['current_order'], + completed=book['completed'], + errors="", + username=username)) + + def get_book_create(self): + cookie = bottle.rddequest.get_cookie("session") + username = self.sessions.get_username(cookie) + return bottle.template("book_template", dict(title="", + id='newentry', + current_order=False, + completed=False, + errors="", + username=username)) + + def post_create_book(self): + book_id = bottle.request.forms.get("id") + book_title = bottle.request.forms.get("title") + book_order = bottle.request.forms.get("current_order") + book_completed = bottle.request.forms.get("completed") + if book_id == "newentry": + self.books.insert_entry(book_title, None, book_order, book_completed) + else: + self.comics.update_entry(book_id, book_title, None, book_order, book_completed) + bottle.redirect("/comics/comic") diff --git a/bottle/library/bookDAO.py b/bottle/library/bookDAO.py new file mode 100644 index 0000000..e057fa0 --- /dev/null +++ b/bottle/library/bookDAO.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from bson.objectid import ObjectId +import sys + +class BookDAO: + + # constructor for the class + def __init__(self, database): + self.db = database + self.books = database.books + + def get_books(self): + cursor = self.books.find() + l = [] + for book in cursor: + l.append( + {'title':book['title'], + '_id':book['_id'], + 'publisher':book['publisher'], + 'isbn':book['isbn'], + 'author':book['author']} + ) + return l + + def get_book(self, book_id): + book = self.books.find_one({"_id": ObjectId(book_id)}) + return book + + def insert_entry(self, book_title): + print "inserting book entry", book_title + + book = {"title": book_title} + + # now insert the book + try: + result = self.books.insert_one(book) + print "Matching book: ", result.matched_count + print "Modified book: ", result.modified_count + except: + print "Error inserting book" + print "Unexpected error:", sys.exc_info() + + def update_entry(self, book_id, book_title): + print "updating book entry", book_title + + filter_doc = {"_id": ObjectId(book_id)} + book = { "$set": {"name": book_title}} + + # now insert the book + try: + result = self.books.update_one(filter_doc, book) + print "Modified book: ", result.modified_count + except: + print "Error inserting book" + print "Unexpected error:", sys.exc_info() + diff --git a/bottle/medien/__init__.py b/bottle/medien/__init__.py new file mode 100644 index 0000000..555e93b --- /dev/null +++ b/bottle/medien/__init__.py @@ -0,0 +1,25 @@ +# -*- coding:utf-8 -*- +import pymongo +import bottle +import cgi + + +__author__ = 'tpeetz' + + +class Plugin: + + def __init__(self, app, database, sessions): + self.app = app + self.db = database + self.sessions = sessions + self.routing() + + def routing(self): + self.app.route('/medien', 'GET', self.media_index) + + def media_index(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + return bottle.template('media_index', dict(username=username)) + diff --git a/bottle/sessionDAO.py b/bottle/sessionDAO.py new file mode 100755 index 0000000..ad98a47 --- /dev/null +++ b/bottle/sessionDAO.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +import sys +import random +import string + + +# The session Data Access Object handles interactions with the sessions collection + +class SessionDAO: + + def __init__(self, database): + self.db = database + self.sessions = database.sessions + + # will start a new session id by adding a new document to the sessions collection + # returns the sessionID or None + def start_session(self, username): + + session_id = self.get_random_str(32) + session = {'username': username, '_id': session_id} + + try: + self.sessions.insert_one(session) + except: + print "Unexpected error on start_session:", sys.exc_info()[0] + return None + + return str(session['_id']) + + # will send a new user session by deleting from sessions table + def end_session(self, session_id): + + if session_id is None: + return + + self.sessions.delete_one({'_id': session_id}) + + return + + # if there is a valid session, it is returned + def get_session(self, session_id): + + if session_id is None: + return None + + session = self.sessions.find_one({'_id': session_id}) + + return session + + # get the username of the current session, or None if the session is not valid + def get_username(self, session_id): + + session = self.get_session(session_id) + if session is None: + return None + else: + return session['username'] + + def get_random_str(self, num_chars): + random_string = "" + for i in range(num_chars): + random_string = random_string + random.choice(string.ascii_letters) + return random_string diff --git a/bottle/setup.py b/bottle/setup.py new file mode 100644 index 0000000..fad97fe --- /dev/null +++ b/bottle/setup.py @@ -0,0 +1,11 @@ +from setuptools import setup + +setup( + name='kontor-bottle', + packages=['comics', 'homeoffice', 'library', 'medien'], + include_package_data=True, + install_requires=[ + 'bottle', + 'pymongo' + ], +) diff --git a/bottle/tradingcards/__init__.py b/bottle/tradingcards/__init__.py new file mode 100644 index 0000000..83931d7 --- /dev/null +++ b/bottle/tradingcards/__init__.py @@ -0,0 +1,25 @@ +# -*- coding:utf-8 -*- +import pymongo +import bottle +import cgi + + +__author__ = 'tpeetz' + + +class Plugin: + + def __init__(self, app, database, sessions): + self.app = app + self.db = database + self.sessions = sessions + self.routing() + + def routing(self): + self.app.route('/tradingcards', 'GET', self.tradingcards_index) + + def tradingcards_index(self): + cookie = bottle.request.get_cookie("session") + username = self.sessions.get_username(cookie) + return bottle.template('tradingcards_index', dict(username=username)) + diff --git a/bottle/userDAO.py b/bottle/userDAO.py new file mode 100755 index 0000000..fec2c8f --- /dev/null +++ b/bottle/userDAO.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +import hmac +import random +import string +import hashlib +import pymongo + + +# The User Data Access Object handles all interactions with the User collection. +class UserDAO: + + def __init__(self, db): + self.db = db + self.users = self.db.users + self.SECRET = 'verysecret' + + # makes a little salt + def make_salt(self): + salt = "" + for i in range(5): + salt = salt + random.choice(string.ascii_letters) + return salt + + # implement the function make_pw_hash(name, pw) that returns a hashed password + # of the format: + # HASH(pw + salt),salt + # use sha256 + + def make_pw_hash(self, pw,salt=None): + if salt == None: + salt = self.make_salt(); + return hashlib.sha256(pw + salt).hexdigest()+","+ salt + + # Validates a user login. Returns user record or None + def validate_login(self, username, password): + + user = None + try: + user = self.users.find_one({'_id': username}) + except: + print "Unable to query database for user" + + if user is None: + print "User not in database" + return None + + salt = user['password'].split(',')[1] + + if user['password'] != self.make_pw_hash(password, salt): + print "user password is not a match" + return None + + # Looks good + return user + + + # creates a new user in the users collection + def add_user(self, username, password, email): + password_hash = self.make_pw_hash(password) + + user = {'_id': username, 'password': password_hash} + if email != "": + user['email'] = email + + try: + self.users.insert_one(user) + except pymongo.errors.OperationFailure: + print "oops, mongo error" + return False + except pymongo.errors.DuplicateKeyError as e: + print "oops, username is already taken" + return False + + return True + + diff --git a/bottle/views/artist_list.tpl b/bottle/views/artist_list.tpl new file mode 100644 index 0000000..366d4b7 --- /dev/null +++ b/bottle/views/artist_list.tpl @@ -0,0 +1,39 @@ + + + +Kontor + + + +
Kontor
+ +
+
+ +
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle/views/artist_template.tpl b/bottle/views/artist_template.tpl new file mode 100644 index 0000000..f981355 --- /dev/null +++ b/bottle/views/artist_template.tpl @@ -0,0 +1,43 @@ + + + +Kontor + + + +
Kontor
+ +
+
+
+ {{errors}} +

Title

+ +
+

+ +

+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + + diff --git a/bottle/views/book_list.tpl b/bottle/views/book_list.tpl new file mode 100644 index 0000000..a6bab72 --- /dev/null +++ b/bottle/views/book_list.tpl @@ -0,0 +1,44 @@ + + + +Kontor + + + +
Kontor
+ +
+
+ + + +%for book in books: + + + +%end + +
TitleCurrent OrderCompleted
{{book['title']}}{{book['current_order']}}{{book['completed']}}
+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle/views/comic_index.tpl b/bottle/views/comic_index.tpl new file mode 100644 index 0000000..529a38e --- /dev/null +++ b/bottle/views/comic_index.tpl @@ -0,0 +1,34 @@ + + + +Kontor + + + +
Kontor
+ +
+
+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle/views/comic_list.tpl b/bottle/views/comic_list.tpl new file mode 100644 index 0000000..a947073 --- /dev/null +++ b/bottle/views/comic_list.tpl @@ -0,0 +1,44 @@ + + + +Kontor + + + +
Kontor
+ +
+
+ + + +%for comic in comics: + + + +%end + +
TitleCurrent OrderCompleted
{{comic['title']}}{{comic['current_order']}}{{comic['completed']}}
+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle/views/comic_template.tpl b/bottle/views/comic_template.tpl new file mode 100644 index 0000000..38f190e --- /dev/null +++ b/bottle/views/comic_template.tpl @@ -0,0 +1,51 @@ + + + +Kontor + + + +
Kontor
+ +
+
+
+ {{errors}} +

Title

+ +
+

+ + +

+ +

+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle/views/kontor.tpl b/bottle/views/kontor.tpl new file mode 100644 index 0000000..76fa397 --- /dev/null +++ b/bottle/views/kontor.tpl @@ -0,0 +1,28 @@ + + + +Kontor + + + +
Kontor
+ +
+
+
+
+%if (username == None): +
Login

Ingenieurbüro Thomas Peetz

+%end +%if (username != None): + +%end + + diff --git a/bottle/views/login.tpl b/bottle/views/login.tpl new file mode 100644 index 0000000..881a1af --- /dev/null +++ b/bottle/views/login.tpl @@ -0,0 +1,52 @@ + + + +Kontor + + + +
Kontor
+ +
+
+
+ + + + + + + + + + + + + +
Username
Password{{login_error}}
+ + +
+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle/views/media_index.tpl b/bottle/views/media_index.tpl new file mode 100644 index 0000000..610e0ff --- /dev/null +++ b/bottle/views/media_index.tpl @@ -0,0 +1,32 @@ + + + +Kontor + + + +
Kontor
+ +
+
+

Medien

+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle/views/office_index.tpl b/bottle/views/office_index.tpl new file mode 100644 index 0000000..6b9e93d --- /dev/null +++ b/bottle/views/office_index.tpl @@ -0,0 +1,35 @@ + + + +Kontor + + + +
Kontor
+ +
+
+

HomeOffice

+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle/views/publisher_list.tpl b/bottle/views/publisher_list.tpl new file mode 100644 index 0000000..7139e17 --- /dev/null +++ b/bottle/views/publisher_list.tpl @@ -0,0 +1,39 @@ + + + +Kontor + + + +
Kontor
+ +
+
+ +
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle/views/publisher_template.tpl b/bottle/views/publisher_template.tpl new file mode 100644 index 0000000..91cbc4a --- /dev/null +++ b/bottle/views/publisher_template.tpl @@ -0,0 +1,42 @@ + + + +Kontor + + + +
Kontor
+ +
+
+
+ {{errors}} +

Title

+ +
+

+ +

+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle/views/signup.tpl b/bottle/views/signup.tpl new file mode 100644 index 0000000..2b3790c --- /dev/null +++ b/bottle/views/signup.tpl @@ -0,0 +1,59 @@ + + + +Kontor + + + +
Kontor
+ +
+
+

Signup

+
+ + + + + + + + + + + + + + + + + + + + +
Username{{username_error}}
Password{{password_error}}
Verify Password{{verify_error}}
Email (optional){{email_error}}
+ +
+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle/views/storyarc_list.tpl b/bottle/views/storyarc_list.tpl new file mode 100644 index 0000000..82a5ce3 --- /dev/null +++ b/bottle/views/storyarc_list.tpl @@ -0,0 +1,40 @@ + + + +Kontor + + + +
Kontor
+ +
+
+ +
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle/views/storyarc_template.tpl b/bottle/views/storyarc_template.tpl new file mode 100644 index 0000000..010d885 --- /dev/null +++ b/bottle/views/storyarc_template.tpl @@ -0,0 +1,43 @@ + + + +Kontor + + + +
Kontor
+ +
+
+
+ {{errors}} +

Title

+ +
+

+ +

+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/bottle/views/tradingcards_index.tpl b/bottle/views/tradingcards_index.tpl new file mode 100644 index 0000000..a576a45 --- /dev/null +++ b/bottle/views/tradingcards_index.tpl @@ -0,0 +1,32 @@ + + + +Kontor + + + +
Kontor
+ +
+
+

Trading Cards

+
+
+
+%if (username == None): + Login +%end +%if (username != None): + {{username}} +%end +

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/django/.gitignore b/django/.gitignore new file mode 100644 index 0000000..4d34ffb --- /dev/null +++ b/django/.gitignore @@ -0,0 +1,16 @@ +bin/ +include/ +lib/ +lib64/ +lib64 +pip-selfcheck.json +<<<<<<< HEAD +<<<<<<< HEAD +*.pyc +kontor/db.sqlite3 +.idea/ +======= +>>>>>>> initial setup +======= +*.pyc +>>>>>>> ignore compiled scripts diff --git a/django/Jenkinsfile b/django/Jenkinsfile new file mode 100644 index 0000000..89b3672 --- /dev/null +++ b/django/Jenkinsfile @@ -0,0 +1,13 @@ +node { + stage "Checkout" + checkout scm + stage "make migrations" + sh "python kontor/manage.py makemigrations" + stage "execute tests" +<<<<<<< HEAD + sh "python kontor/manage.py test comics library medien tradingcards homeoffice" +======= + sh "python kontor/manage.py test commics library medien tradingcards homeoffice" +>>>>>>> add tests +} + diff --git a/django/README.md b/django/README.md new file mode 100644 index 0000000..708d77d --- /dev/null +++ b/django/README.md @@ -0,0 +1,2 @@ +# Kontor Django + diff --git a/django/kontor/comics/__init__.py b/django/kontor/comics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/kontor/comics/admin.py b/django/kontor/comics/admin.py new file mode 100755 index 0000000..0f96082 --- /dev/null +++ b/django/kontor/comics/admin.py @@ -0,0 +1,86 @@ +from django.contrib import admin + +<<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> add Comic models +from .models import Publisher +from .models import Artist +from .models import Issue +from .models import StoryArc +from .models import Comic +from .models import TradePaperback +from .models import Volume + +<<<<<<< HEAD +# Register your models here. + + +class IssueInline(admin.StackedInline): + model = Issue + extra = 1 + + +class IssueAdmin(admin.ModelAdmin): + fields = ['comic', 'number', 'is_read', 'in_stock'] + + +class ComicAdmin(admin.ModelAdmin): + fields = ['title', 'publisher', 'writer', 'current_order', 'completed'] + inlines = [IssueInline] + + +class ComicInline(admin.StackedInline): + model = Comic + extra = 1 + + +class PublisherAdmin(admin.ModelAdmin): + inlines = [ComicInline] + + +admin.site.register(Publisher, PublisherAdmin) +admin.site.register(Artist) +admin.site.register(Issue, IssueAdmin) +admin.site.register(Volume) +admin.site.register(StoryArc) +admin.site.register(Comic, ComicAdmin) +admin.site.register(TradePaperback) +======= +# Register your models here. +>>>>>>> initial setup +======= +# Register your models here. + + +class IssueInline(admin.StackedInline): + model = Issue + extra = 1 + + +class IssueAdmin(admin.ModelAdmin): + fields = ['comic', 'number', 'is_read', 'in_stock'] + + +class ComicAdmin(admin.ModelAdmin): + fields = ['title', 'publisher', 'writer', 'artist', 'current_order', 'completed'] + inlines = [IssueInline] + + +class ComicInline(admin.StackedInline): + model = Comic + extra = 1 + + +class PublisherAdmin(admin.ModelAdmin): + inlines = [ComicInline] + + +admin.site.register(Publisher, PublisherAdmin) +admin.site.register(Artist) +admin.site.register(Issue, IssueAdmin) +admin.site.register(Volume) +admin.site.register(StoryArc) +admin.site.register(Comic, ComicAdmin) +admin.site.register(TradePaperback) +>>>>>>> add Comic models diff --git a/django/kontor/comics/apps.py b/django/kontor/comics/apps.py new file mode 100644 index 0000000..7c7e1bf --- /dev/null +++ b/django/kontor/comics/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ComicsConfig(AppConfig): + name = 'comics' diff --git a/django/kontor/comics/fixtures/comics.yaml b/django/kontor/comics/fixtures/comics.yaml new file mode 100755 index 0000000..0c20c1f --- /dev/null +++ b/django/kontor/comics/fixtures/comics.yaml @@ -0,0 +1,3048 @@ +- model: comics.publisher + pk: 1 + fields: {name: Marvel} +- model: comics.publisher + pk: 2 + fields: {name: Alias} +- model: comics.publisher + pk: 3 + fields: {name: Crossgen} +- model: comics.publisher + pk: 4 + fields: {name: Image} +- model: comics.publisher + pk: 5 + fields: {name: Devils Due Publishing} +- model: comics.publisher + pk: 6 + fields: {name: Aspen} +- model: comics.publisher + pk: 7 + fields: {name: Bongo Comics} +- model: comics.publisher + pk: 8 + fields: {name: Kandora} +- model: comics.publisher + pk: 9 + fields: {name: DC} +- model: comics.publisher + pk: 10 + fields: {name: Marvel Knights} +- model: comics.publisher + pk: 11 + fields: {name: WildStorm} +- model: comics.publisher + pk: 12 + fields: {name: Cliffhanger} +- model: comics.publisher + pk: 13 + fields: {name: Dark Horse Comics} +- model: comics.publisher + pk: 14 + fields: {name: Broadsword} +- model: comics.publisher + pk: 15 + fields: {name: Dynamite Entertainment} +- model: comics.publisher + pk: 16 + fields: {name: Red Eagle Entertainment} +- model: comics.publisher + pk: 17 + fields: {name: Top Cow Productions} +- model: comics.publisher + pk: 18 + fields: {name: Pulp Fiction} +- model: comics.artist + pk: 1 + fields: {name: 'Turner, Michael'} +- model: comics.artist + pk: 2 + fields: {name: 'Marz, Ron'} +- model: comics.comic + pk: 1 + fields: {title: '1602', publisher: 1, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 2 + fields: {title: 10th Muse, publisher: 2, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 3 + fields: {title: Abadazad, publisher: 3, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 4 + fields: {title: Amazing Fantasy, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 5 + fields: {title: Amazing Spider-Man, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 6 + fields: {title: Arana, publisher: 1, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 7 + fields: {title: Aria, publisher: 4, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 8 + fields: {title: Army of Darkness, publisher: 5, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 9 + fields: {title: Aspen, publisher: 6, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 10 + fields: {title: Astonishing X-Men, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 11 + fields: {title: Athena Inc. The Beginning, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 12 + fields: {title: Athena Inc. The Manhunter Project, publisher: 4, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 13 + fields: {title: Barbarossa & The Lost Corsairs, publisher: 8, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 14 + fields: {title: Bart Simpson, publisher: 7, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 15 + fields: {title: Bart Simpsons Treehouse of Horror, publisher: 7, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 16 + fields: {title: Battle Pope, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 17 + fields: {title: Birds of Prey, publisher: 9, current_order: false, completed: false, + writer: 1, artist: null} +- model: comics.comic + pk: 18 + fields: {title: Black Widow, publisher: 10, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 19 + fields: {title: Black Widow 2, publisher: 10, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 20 + fields: {title: Bluntman and Chronic, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 21 + fields: {title: Brath, publisher: 3, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 22 + fields: {title: Catwoman When In Rome, publisher: 9, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 23 + fields: {title: Crimson, publisher: 11, current_order: false, completed: false, + writer: 1, artist: null} +- model: comics.comic + pk: 24 + fields: {title: Crossgen, publisher: 3, current_order: false, completed: false, + writer: 1, artist: null} +- model: comics.comic + pk: 25 + fields: {title: Danger Girl, publisher: 12, current_order: false, completed: false, + writer: 1, artist: null} +- model: comics.comic + pk: 26 + fields: {title: Danger Girl Back in Black, publisher: 11, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 27 + fields: {title: Daring Escapes, publisher: 4, current_order: false, completed: true, + writer: null, artist: null} +- model: comics.comic + pk: 28 + fields: {title: Darkness / Superman, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 29 + fields: {title: Darkness / Tomb Raider, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 30 + fields: {title: Darkness / Vampirella, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 31 + fields: {title: Darkness Black Sails, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 32 + fields: {title: Darkness Vol. 2, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 33 + fields: {title: District X, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 34 + fields: {title: 'Dragonlance: Chronicles', publisher: 5, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 35 + fields: {title: Dream Police, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 36 + fields: {title: El Cazador, publisher: 3, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 37 + fields: {title: El Cazador The Bloody Ballad of Blackjack Tom, publisher: 3, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 38 + fields: {title: Elsinore, publisher: 2, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 39 + fields: {title: Emma Frost, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 40 + fields: {title: Excalibur, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 41 + fields: {title: Fathom, publisher: 6, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 42 + fields: {title: Fathom Beginnings, publisher: 6, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 43 + fields: {title: Fathom Cannon Hawke, publisher: 6, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 44 + fields: {title: Fathom Cannon Hawke Prelude, publisher: 6, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 45 + fields: {title: Fathom Dawn of War, publisher: 6, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 46 + fields: {title: Fathom Prelude, publisher: 6, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 47 + fields: {title: Fathom Swimsuit Special, publisher: 6, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 48 + fields: {title: Fathom Swimsuit Special 2000, publisher: 6, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 49 + fields: {title: Fathom Vol. 2, publisher: 6, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 50 + fields: {title: 'Fathom: Killians Tide', publisher: 6, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 51 + fields: {title: Flak Riot, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 52 + fields: {title: Freshmen, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 53 + fields: {title: Friendly Neighborhood Spider-Man, publisher: 1, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 54 + fields: {title: Futurama, publisher: 7, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 55 + fields: {title: Futurama Simpsons Crossover Crisis Part 2, publisher: 7, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 56 + fields: {title: Ghostrider, publisher: 10, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 57 + fields: {title: Gift, publisher: 4, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 58 + fields: {title: Hack Slash Land of Lost Toys, publisher: 5, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 59 + fields: {title: 'Hack/Slash: Girls Gone Dead', publisher: 5, current_order: false, + completed: false, writer: 1, artist: null} +- model: comics.comic + pk: 60 + fields: {title: Harry Johnson, publisher: 18, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 61 + fields: {title: Hellcop, publisher: 4, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 62 + fields: {title: Holiday Special 2004, publisher: 1, current_order: false, completed: false, + writer: 1, artist: null} +- model: comics.comic + pk: 63 + fields: {title: House of M, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 64 + fields: {title: Hunter-Killer, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 65 + fields: {title: Hunter-Killer Dossier, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 66 + fields: {title: Iron Ghost, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 67 + fields: {title: 'J.U.D.G.E.: Secret Rage', publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 68 + fields: {title: Kiss Kiss Bang Bang, publisher: 3, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 69 + fields: {title: Legend of Isis, publisher: 2, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 70 + fields: {title: Loki, publisher: 1, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 71 + fields: {title: Lullaby, publisher: 4, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 72 + fields: {title: Magdalena / Vampirella 2, publisher: 17, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 73 + fields: {title: Marvel Knights Spider-Man, publisher: 10, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 74 + fields: {title: Marville, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 75 + fields: {title: Mary Jane, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 76 + fields: {title: Mary Jane Homecoming, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 77 + fields: {title: Megacity 909, publisher: 5, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 78 + fields: {title: Meridian, publisher: 3, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 79 + fields: {title: Midnight Nation, publisher: 4, current_order: false, completed: true, + writer: 1, artist: null} +- model: comics.comic + pk: 80 + fields: {title: Monster War, publisher: 4, current_order: false, completed: false, + writer: 1, artist: null} +- model: comics.comic + pk: 81 + fields: {title: Monster War 2005, publisher: 4, current_order: false, completed: false, + writer: 1, artist: null} +- model: comics.comic + pk: 82 + fields: {title: Mystic, publisher: 3, current_order: false, completed: false, writer: 1, + artist: null} +- model: comics.comic + pk: 83 + fields: {title: Mystique, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 84 + fields: {title: Necromancer, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 85 + fields: {title: Negation War, publisher: 3, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 86 + fields: {title: New Avengers, publisher: 1, current_order: false, completed: false, + writer: 1, artist: null} +- model: comics.comic + pk: 87 + fields: {title: New Mutants, publisher: 1, current_order: false, completed: false, + writer: 1, artist: null} +- model: comics.comic + pk: 88 + fields: {title: New X-Men, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 89 + fields: {title: New X-Men Academy X, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 90 + fields: {title: New X-Men Hellions, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 91 + fields: {title: Nightcrawler, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 92 + fields: {title: 'Ororo: Before the Storm', publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 93 + fields: {title: Radix, publisher: 4, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 94 + fields: {title: Red Sonja, publisher: 15, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 95 + fields: {title: Revelations, publisher: 13, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 96 + fields: {title: Rogue, publisher: 1, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 97 + fields: {title: Ruse, publisher: 3, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 98 + fields: {title: 'Samurai: Heaven & Earth', publisher: 13, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 99 + fields: {title: Scion, publisher: 3, current_order: false, completed: false, writer: 1, + artist: null} +- model: comics.comic + pk: 100 + fields: {title: 'Shanna, The She-Devil', publisher: 10, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 101 + fields: {title: She-Hulk, publisher: 1, current_order: false, completed: false, + writer: 1, artist: null} +- model: comics.comic + pk: 102 + fields: {title: She-Hulk 2, publisher: 1, current_order: false, completed: false, + writer: 1, artist: null} +- model: comics.comic + pk: 103 + fields: {title: Shi Ju-Nen, publisher: 13, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 104 + fields: {title: Shrek, publisher: 13, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 105 + fields: {title: Simpsons, publisher: 7, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 106 + fields: {title: Sojourn, publisher: 3, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 107 + fields: {title: Solus, publisher: 3, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 108 + fields: {title: Soulfire, publisher: 6, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 109 + fields: {title: Soulfire Dying of the Light, publisher: 6, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 110 + fields: {title: Spectacular Spider-Man, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 111 + fields: {title: Spellbinders, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 112 + fields: {title: Spider-Man India, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 113 + fields: {title: Spider-Man loves Mary Jane, publisher: 1, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 114 + fields: {title: Spider-Man Team Up, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 115 + fields: {title: 'Spider-Man: Breakout', publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 116 + fields: {title: 'Spider-Man: House of M', publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 117 + fields: {title: Star Wars, publisher: 13, current_order: false, completed: false, + writer: 1, artist: null} +- model: comics.comic + pk: 118 + fields: {title: Stardust Kid, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 119 + fields: {title: Strange, publisher: 1, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 120 + fields: {title: Supergirl, publisher: 9, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 121 + fields: {title: Superman, publisher: 9, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 122 + fields: {title: Superman/Batman, publisher: 9, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 123 + fields: {title: Tarot Witch of the Black Rose, publisher: 14, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 124 + fields: {title: The Art of Greg Horn, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 125 + fields: {title: The Devil's Keeper, publisher: 2, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 126 + fields: {title: The Tenth, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 127 + fields: {title: The Tomb of Dracula, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 128 + fields: {title: 'Robert Jordan''s The Wheel of Time: New Spring', publisher: 16, + current_order: false, completed: false, writer: null, artist: null} +- model: comics.comic + pk: 129 + fields: {title: 'Thor: Son of Asgard', publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 130 + fields: {title: Tom Strong, publisher: 11, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 131 + fields: {title: Tomb Raider, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 132 + fields: {title: 'Tomb Raider: The Greatest Treasure of All', publisher: 4, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 133 + fields: {title: Toxin, publisher: 1, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 134 + fields: {title: Ultimate Fantastic Four, publisher: 1, current_order: false, completed: false, + writer: 1, artist: null} +- model: comics.comic + pk: 135 + fields: {title: Ultimate Spider-Man Annual, publisher: 1, current_order: false, + completed: false, writer: 1, artist: null} +- model: comics.comic + pk: 136 + fields: {title: Uncanny X-Men, publisher: 1, current_order: false, completed: false, + writer: 1, artist: null} +- model: comics.comic + pk: 137 + fields: {title: Vampirella, publisher: 15, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 138 + fields: {title: Wild Girl, publisher: 11, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 139 + fields: {title: 'Wildcats: Nemesis', publisher: 11, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 140 + fields: {title: Wildsiderz, publisher: 11, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 141 + fields: {title: Witchblade, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 142 + fields: {title: Witchblade / Tomb Raider, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 143 + fields: {title: 'Wolverine: The End', publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 144 + fields: {title: Wood Boy, publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 145 + fields: {title: Wraithborn, publisher: 11, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 146 + fields: {title: X-23, publisher: 1, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 147 + fields: {title: 'X-Men: Age of Apocalypse', publisher: 1, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 148 + fields: {title: 'X-Men: Age of Apocalypse One Shot', publisher: 1, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 149 + fields: {title: 'X-Men: Kitty Pryde', publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 150 + fields: {title: 'X-Men: Phoenix - Endsong', publisher: 1, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 151 + fields: {title: X-treme X-Men, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 152 + fields: {title: Army of Darkness vs. Re-Animator, publisher: 15, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 153 + fields: {title: Runaways, publisher: 1, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 154 + fields: {title: Crux, publisher: 3, current_order: false, completed: false, writer: null, + artist: null} +- model: comics.comic + pk: 155 + fields: {title: 'Aria: The Soul Market', publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 156 + fields: {title: 'Aria: Summer''s Spell', publisher: 4, current_order: false, completed: false, + writer: null, artist: null} +- model: comics.comic + pk: 157 + fields: {title: 'Aria: The Uses of Enchantment', publisher: 4, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 158 + fields: {title: 'Army of Darkness: Ashes 2 Ashes', publisher: 13, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.comic + pk: 159 + fields: {title: 'Army of Darkness: Shop Till You Drop Dead', publisher: 13, current_order: false, + completed: false, writer: null, artist: null} +- model: comics.issue + pk: 1 + fields: {number: '1', is_read: false, in_stock: false, comic: 150, volume: null} +- model: comics.issue + pk: 2 + fields: {number: '2', is_read: false, in_stock: false, comic: 150, volume: null} +- model: comics.issue + pk: 3 + fields: {number: '3', is_read: false, in_stock: false, comic: 150, volume: null} +- model: comics.issue + pk: 4 + fields: {number: '4', is_read: false, in_stock: false, comic: 150, volume: null} +- model: comics.issue + pk: 5 + fields: {number: '5', is_read: false, in_stock: false, comic: 150, volume: null} +- model: comics.issue + pk: 6 + fields: {number: '1', is_read: true, in_stock: false, comic: 79, volume: null} +- model: comics.issue + pk: 7 + fields: {number: '2', is_read: true, in_stock: false, comic: 79, volume: null} +- model: comics.issue + pk: 8 + fields: {number: '3', is_read: true, in_stock: false, comic: 79, volume: null} +- model: comics.issue + pk: 9 + fields: {number: '4', is_read: true, in_stock: false, comic: 79, volume: null} +- model: comics.issue + pk: 10 + fields: {number: '5', is_read: true, in_stock: false, comic: 79, volume: null} +- model: comics.issue + pk: 11 + fields: {number: '6', is_read: true, in_stock: false, comic: 79, volume: null} +- model: comics.issue + pk: 12 + fields: {number: '7', is_read: true, in_stock: false, comic: 79, volume: null} +- model: comics.issue + pk: 13 + fields: {number: '8', is_read: true, in_stock: false, comic: 79, volume: null} +- model: comics.issue + pk: 14 + fields: {number: '9', is_read: true, in_stock: false, comic: 79, volume: null} +- model: comics.issue + pk: 15 + fields: {number: '10', is_read: true, in_stock: false, comic: 79, volume: null} +- model: comics.issue + pk: 16 + fields: {number: '11', is_read: true, in_stock: false, comic: 79, volume: null} +- model: comics.issue + pk: 17 + fields: {number: '12', is_read: true, in_stock: false, comic: 79, volume: null} +- model: comics.issue + pk: 18 + fields: {number: '1', is_read: false, in_stock: false, comic: 6, volume: null} +- model: comics.issue + pk: 19 + fields: {number: '2', is_read: false, in_stock: false, comic: 6, volume: null} +- model: comics.issue + pk: 20 + fields: {number: '3', is_read: false, in_stock: false, comic: 6, volume: null} +- model: comics.issue + pk: 21 + fields: {number: '4', is_read: false, in_stock: false, comic: 6, volume: null} +- model: comics.issue + pk: 22 + fields: {number: '5', is_read: false, in_stock: false, comic: 6, volume: null} +- model: comics.issue + pk: 23 + fields: {number: '6', is_read: false, in_stock: false, comic: 6, volume: null} +- model: comics.issue + pk: 24 + fields: {number: '7', is_read: false, in_stock: false, comic: 6, volume: null} +- model: comics.issue + pk: 25 + fields: {number: '8', is_read: false, in_stock: false, comic: 6, volume: null} +- model: comics.issue + pk: 26 + fields: {number: '9', is_read: false, in_stock: false, comic: 6, volume: null} +- model: comics.issue + pk: 27 + fields: {number: '10', is_read: false, in_stock: false, comic: 6, volume: null} +- model: comics.issue + pk: 28 + fields: {number: '11', is_read: false, in_stock: false, comic: 6, volume: null} +- model: comics.issue + pk: 29 + fields: {number: '1', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 30 + fields: {number: '2', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 31 + fields: {number: '3', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 32 + fields: {number: '4', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 33 + fields: {number: '5', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 34 + fields: {number: '6', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 35 + fields: {number: '7', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 36 + fields: {number: '8', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 37 + fields: {number: '9', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 38 + fields: {number: '10', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 39 + fields: {number: '11', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 40 + fields: {number: '12', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 41 + fields: {number: '13', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 42 + fields: {number: '14', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 43 + fields: {number: '15', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 44 + fields: {number: '16', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 45 + fields: {number: '17', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 46 + fields: {number: '18', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 47 + fields: {number: '19', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 48 + fields: {number: '20', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 49 + fields: {number: '21', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 50 + fields: {number: '22', is_read: false, in_stock: false, comic: 54, volume: null} +- model: comics.issue + pk: 51 + fields: {number: '1', is_read: false, in_stock: false, comic: 55, volume: null} +- model: comics.issue + pk: 52 + fields: {number: '2', is_read: false, in_stock: false, comic: 55, volume: null} +- model: comics.issue + pk: 53 + fields: {number: '1', is_read: false, in_stock: false, comic: 16, volume: null} +- model: comics.issue + pk: 54 + fields: {number: '2', is_read: false, in_stock: false, comic: 16, volume: null} +- model: comics.issue + pk: 55 + fields: {number: '3', is_read: false, in_stock: false, comic: 16, volume: null} +- model: comics.issue + pk: 56 + fields: {number: '11', is_read: false, in_stock: false, comic: 15, volume: null} +- model: comics.issue + pk: 57 + fields: {number: '20', is_read: false, in_stock: false, comic: 14, volume: null} +- model: comics.issue + pk: 58 + fields: {number: '21', is_read: false, in_stock: false, comic: 14, volume: null} +- model: comics.issue + pk: 59 + fields: {number: '22', is_read: false, in_stock: false, comic: 14, volume: null} +- model: comics.issue + pk: 60 + fields: {number: '23', is_read: false, in_stock: false, comic: 14, volume: null} +- model: comics.issue + pk: 61 + fields: {number: '24', is_read: false, in_stock: false, comic: 14, volume: null} +- model: comics.issue + pk: 62 + fields: {number: '25', is_read: false, in_stock: false, comic: 14, volume: null} +- model: comics.issue + pk: 63 + fields: {number: '1', is_read: false, in_stock: false, comic: 11, volume: null} +- model: comics.issue + pk: 64 + fields: {number: '1', is_read: false, in_stock: false, comic: 12, volume: null} +- model: comics.issue + pk: 65 + fields: {number: '2', is_read: false, in_stock: false, comic: 12, volume: null} +- model: comics.issue + pk: 66 + fields: {number: '3', is_read: false, in_stock: false, comic: 12, volume: null} +- model: comics.issue + pk: 67 + fields: {number: '4', is_read: false, in_stock: false, comic: 12, volume: null} +- model: comics.issue + pk: 68 + fields: {number: '5', is_read: false, in_stock: false, comic: 12, volume: null} +- model: comics.issue + pk: 69 + fields: {number: '6', is_read: false, in_stock: false, comic: 12, volume: null} +- model: comics.issue + pk: 70 + fields: {number: '1', is_read: false, in_stock: false, comic: 18, volume: null} +- model: comics.issue + pk: 71 + fields: {number: '2', is_read: false, in_stock: false, comic: 18, volume: null} +- model: comics.issue + pk: 72 + fields: {number: '3', is_read: false, in_stock: false, comic: 18, volume: null} +- model: comics.issue + pk: 73 + fields: {number: '4', is_read: false, in_stock: false, comic: 18, volume: null} +- model: comics.issue + pk: 74 + fields: {number: '5', is_read: false, in_stock: false, comic: 18, volume: null} +- model: comics.issue + pk: 75 + fields: {number: '6', is_read: false, in_stock: false, comic: 18, volume: null} +- model: comics.issue + pk: 76 + fields: {number: '1', is_read: false, in_stock: false, comic: 19, volume: null} +- model: comics.issue + pk: 77 + fields: {number: '2', is_read: false, in_stock: false, comic: 19, volume: null} +- model: comics.issue + pk: 78 + fields: {number: '3', is_read: false, in_stock: false, comic: 19, volume: null} +- model: comics.issue + pk: 79 + fields: {number: '4', is_read: false, in_stock: false, comic: 19, volume: null} +- model: comics.issue + pk: 80 + fields: {number: '5', is_read: false, in_stock: false, comic: 19, volume: null} +- model: comics.issue + pk: 81 + fields: {number: '1', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 82 + fields: {number: '2', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 83 + fields: {number: '3', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 84 + fields: {number: '4', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 85 + fields: {number: '5', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 86 + fields: {number: '6', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 87 + fields: {number: '7', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 88 + fields: {number: '8', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 89 + fields: {number: '9', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 90 + fields: {number: '10', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 91 + fields: {number: '11', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 92 + fields: {number: '12', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 93 + fields: {number: '13', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 94 + fields: {number: '14', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 95 + fields: {number: '15', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 96 + fields: {number: '16', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 97 + fields: {number: '17', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 98 + fields: {number: '18', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 99 + fields: {number: '19', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 100 + fields: {number: '20', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 101 + fields: {number: '21', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 102 + fields: {number: '22', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 103 + fields: {number: '23', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 104 + fields: {number: '24', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 105 + fields: {number: '25', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 106 + fields: {number: '26', is_read: false, in_stock: false, comic: 97, volume: null} +- model: comics.issue + pk: 107 + fields: {number: '1', is_read: false, in_stock: false, comic: 98, volume: null} +- model: comics.issue + pk: 108 + fields: {number: '2', is_read: false, in_stock: false, comic: 98, volume: null} +- model: comics.issue + pk: 109 + fields: {number: '3', is_read: false, in_stock: false, comic: 98, volume: null} +- model: comics.issue + pk: 110 + fields: {number: '4', is_read: false, in_stock: false, comic: 98, volume: null} +- model: comics.issue + pk: 111 + fields: {number: '1', is_read: false, in_stock: false, comic: 4, volume: null} +- model: comics.issue + pk: 112 + fields: {number: '2', is_read: false, in_stock: false, comic: 4, volume: null} +- model: comics.issue + pk: 113 + fields: {number: '3', is_read: false, in_stock: false, comic: 4, volume: null} +- model: comics.issue + pk: 114 + fields: {number: '4', is_read: false, in_stock: false, comic: 4, volume: null} +- model: comics.issue + pk: 115 + fields: {number: '5', is_read: false, in_stock: false, comic: 4, volume: null} +- model: comics.issue + pk: 116 + fields: {number: '6', is_read: false, in_stock: false, comic: 4, volume: null} +- model: comics.issue + pk: 117 + fields: {number: '7', is_read: false, in_stock: false, comic: 4, volume: null} +- model: comics.issue + pk: 118 + fields: {number: '8', is_read: false, in_stock: false, comic: 4, volume: null} +- model: comics.issue + pk: 119 + fields: {number: '9', is_read: false, in_stock: false, comic: 4, volume: null} +- model: comics.issue + pk: 120 + fields: {number: '10', is_read: false, in_stock: false, comic: 4, volume: null} +- model: comics.issue + pk: 121 + fields: {number: '11', is_read: false, in_stock: false, comic: 4, volume: null} +- model: comics.issue + pk: 122 + fields: {number: '12', is_read: false, in_stock: false, comic: 4, volume: null} +- model: comics.issue + pk: 123 + fields: {number: '13', is_read: false, in_stock: false, comic: 4, volume: null} +- model: comics.issue + pk: 124 + fields: {number: '14', is_read: false, in_stock: false, comic: 4, volume: null} +- model: comics.issue + pk: 125 + fields: {number: '15', is_read: false, in_stock: false, comic: 4, volume: null} +- model: comics.issue + pk: 126 + fields: {number: '1', is_read: false, in_stock: false, comic: 40, volume: null} +- model: comics.issue + pk: 127 + fields: {number: '2', is_read: false, in_stock: false, comic: 40, volume: null} +- model: comics.issue + pk: 128 + fields: {number: '3', is_read: false, in_stock: false, comic: 40, volume: null} +- model: comics.issue + pk: 129 + fields: {number: '4', is_read: false, in_stock: false, comic: 40, volume: null} +- model: comics.issue + pk: 130 + fields: {number: '5', is_read: false, in_stock: false, comic: 40, volume: null} +- model: comics.issue + pk: 131 + fields: {number: '6', is_read: false, in_stock: false, comic: 40, volume: null} +- model: comics.issue + pk: 132 + fields: {number: '7', is_read: false, in_stock: false, comic: 40, volume: null} +- model: comics.issue + pk: 133 + fields: {number: '8', is_read: false, in_stock: false, comic: 40, volume: null} +- model: comics.issue + pk: 134 + fields: {number: '9', is_read: false, in_stock: false, comic: 40, volume: null} +- model: comics.issue + pk: 135 + fields: {number: '10', is_read: false, in_stock: false, comic: 40, volume: null} +- model: comics.issue + pk: 136 + fields: {number: '11', is_read: false, in_stock: false, comic: 40, volume: null} +- model: comics.issue + pk: 137 + fields: {number: '12', is_read: false, in_stock: false, comic: 40, volume: null} +- model: comics.issue + pk: 138 + fields: {number: '1', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 139 + fields: {number: '2', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 140 + fields: {number: '3', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 141 + fields: {number: '4', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 142 + fields: {number: '5', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 143 + fields: {number: '6', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 144 + fields: {number: '7', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 145 + fields: {number: '8', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 146 + fields: {number: '9', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 147 + fields: {number: '10', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 148 + fields: {number: '11', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 149 + fields: {number: '12', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 150 + fields: {number: '13', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 151 + fields: {number: '14', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 152 + fields: {number: '15', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 153 + fields: {number: '16', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 154 + fields: {number: '17', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 155 + fields: {number: '18', is_read: false, in_stock: false, comic: 39, volume: null} +- model: comics.issue + pk: 156 + fields: {number: '1', is_read: false, in_stock: false, comic: 22, volume: null} +- model: comics.issue + pk: 157 + fields: {number: '2', is_read: false, in_stock: false, comic: 22, volume: null} +- model: comics.issue + pk: 158 + fields: {number: '3', is_read: false, in_stock: false, comic: 22, volume: null} +- model: comics.issue + pk: 159 + fields: {number: '4', is_read: false, in_stock: false, comic: 22, volume: null} +- model: comics.issue + pk: 160 + fields: {number: '5', is_read: false, in_stock: false, comic: 22, volume: null} +- model: comics.issue + pk: 161 + fields: {number: '6', is_read: false, in_stock: false, comic: 22, volume: null} +- model: comics.issue + pk: 162 + fields: {number: '1', is_read: false, in_stock: false, comic: 33, volume: null} +- model: comics.issue + pk: 163 + fields: {number: '2', is_read: false, in_stock: false, comic: 33, volume: null} +- model: comics.issue + pk: 164 + fields: {number: '3', is_read: false, in_stock: false, comic: 33, volume: null} +- model: comics.issue + pk: 165 + fields: {number: '4', is_read: false, in_stock: false, comic: 33, volume: null} +- model: comics.issue + pk: 166 + fields: {number: '5', is_read: false, in_stock: false, comic: 33, volume: null} +- model: comics.issue + pk: 167 + fields: {number: '6', is_read: false, in_stock: false, comic: 33, volume: null} +- model: comics.issue + pk: 168 + fields: {number: '7', is_read: false, in_stock: false, comic: 33, volume: null} +- model: comics.issue + pk: 169 + fields: {number: '8', is_read: false, in_stock: false, comic: 33, volume: null} +- model: comics.issue + pk: 170 + fields: {number: '9', is_read: false, in_stock: false, comic: 33, volume: null} +- model: comics.issue + pk: 171 + fields: {number: '10', is_read: false, in_stock: false, comic: 33, volume: null} +- model: comics.issue + pk: 172 + fields: {number: '11', is_read: false, in_stock: false, comic: 33, volume: null} +- model: comics.issue + pk: 173 + fields: {number: '12', is_read: false, in_stock: false, comic: 33, volume: null} +- model: comics.issue + pk: 174 + fields: {number: '13', is_read: false, in_stock: false, comic: 33, volume: null} +- model: comics.issue + pk: 175 + fields: {number: '14', is_read: false, in_stock: false, comic: 33, volume: null} +- model: comics.issue + pk: 176 + fields: {number: '1', is_read: false, in_stock: false, comic: 36, volume: null} +- model: comics.issue + pk: 177 + fields: {number: '2', is_read: false, in_stock: false, comic: 36, volume: null} +- model: comics.issue + pk: 178 + fields: {number: '3', is_read: false, in_stock: false, comic: 36, volume: null} +- model: comics.issue + pk: 179 + fields: {number: '4', is_read: false, in_stock: false, comic: 36, volume: null} +- model: comics.issue + pk: 180 + fields: {number: '5', is_read: false, in_stock: false, comic: 36, volume: null} +- model: comics.issue + pk: 181 + fields: {number: '6', is_read: false, in_stock: false, comic: 36, volume: null} +- model: comics.issue + pk: 182 + fields: {number: '1', is_read: false, in_stock: false, comic: 37, volume: null} +- model: comics.issue + pk: 183 + fields: {number: '1', is_read: false, in_stock: false, comic: 38, volume: null} +- model: comics.issue + pk: 184 + fields: {number: '1', is_read: false, in_stock: false, comic: 35, volume: null} +- model: comics.issue + pk: 185 + fields: {number: '1', is_read: false, in_stock: false, comic: 34, volume: null} +- model: comics.issue + pk: 186 + fields: {number: '2', is_read: false, in_stock: false, comic: 34, volume: null} +- model: comics.issue + pk: 187 + fields: {number: '1', is_read: false, in_stock: false, comic: 56, volume: null} +- model: comics.issue + pk: 188 + fields: {number: '2', is_read: false, in_stock: false, comic: 56, volume: null} +- model: comics.issue + pk: 189 + fields: {number: '3', is_read: false, in_stock: false, comic: 56, volume: null} +- model: comics.issue + pk: 190 + fields: {number: '4', is_read: false, in_stock: false, comic: 56, volume: null} +- model: comics.issue + pk: 191 + fields: {number: '5', is_read: false, in_stock: false, comic: 56, volume: null} +- model: comics.issue + pk: 192 + fields: {number: '6', is_read: false, in_stock: false, comic: 56, volume: null} +- model: comics.issue + pk: 193 + fields: {number: '1', is_read: false, in_stock: false, comic: 50, volume: null} +- model: comics.issue + pk: 194 + fields: {number: '2', is_read: false, in_stock: false, comic: 50, volume: null} +- model: comics.issue + pk: 195 + fields: {number: '3', is_read: false, in_stock: false, comic: 50, volume: null} +- model: comics.issue + pk: 196 + fields: {number: '4', is_read: false, in_stock: false, comic: 50, volume: null} +- model: comics.issue + pk: 197 + fields: {number: '1', is_read: false, in_stock: false, comic: 75, volume: null} +- model: comics.issue + pk: 198 + fields: {number: '2', is_read: false, in_stock: false, comic: 75, volume: null} +- model: comics.issue + pk: 199 + fields: {number: '3', is_read: false, in_stock: false, comic: 75, volume: null} +- model: comics.issue + pk: 200 + fields: {number: '4', is_read: false, in_stock: false, comic: 75, volume: null} +- model: comics.issue + pk: 201 + fields: {number: '1', is_read: false, in_stock: false, comic: 76, volume: null} +- model: comics.issue + pk: 202 + fields: {number: '2', is_read: false, in_stock: false, comic: 76, volume: null} +- model: comics.issue + pk: 203 + fields: {number: '3', is_read: false, in_stock: false, comic: 76, volume: null} +- model: comics.issue + pk: 204 + fields: {number: '4', is_read: false, in_stock: false, comic: 76, volume: null} +- model: comics.issue + pk: 205 + fields: {number: '1', is_read: false, in_stock: false, comic: 74, volume: null} +- model: comics.issue + pk: 206 + fields: {number: '2', is_read: false, in_stock: false, comic: 74, volume: null} +- model: comics.issue + pk: 207 + fields: {number: '3', is_read: false, in_stock: false, comic: 74, volume: null} +- model: comics.issue + pk: 208 + fields: {number: '4', is_read: false, in_stock: false, comic: 74, volume: null} +- model: comics.issue + pk: 209 + fields: {number: '5', is_read: false, in_stock: false, comic: 74, volume: null} +- model: comics.issue + pk: 210 + fields: {number: '6', is_read: false, in_stock: false, comic: 74, volume: null} +- model: comics.issue + pk: 211 + fields: {number: '7', is_read: false, in_stock: false, comic: 74, volume: null} +- model: comics.issue + pk: 212 + fields: {number: '1', is_read: false, in_stock: false, comic: 77, volume: null} +- model: comics.issue + pk: 213 + fields: {number: '2', is_read: false, in_stock: false, comic: 77, volume: null} +- model: comics.issue + pk: 214 + fields: {number: '3', is_read: false, in_stock: false, comic: 77, volume: null} +- model: comics.issue + pk: 215 + fields: {number: '4', is_read: false, in_stock: false, comic: 77, volume: null} +- model: comics.issue + pk: 216 + fields: {number: '5', is_read: false, in_stock: false, comic: 77, volume: null} +- model: comics.issue + pk: 217 + fields: {number: '6', is_read: false, in_stock: false, comic: 77, volume: null} +- model: comics.issue + pk: 218 + fields: {number: '7', is_read: false, in_stock: false, comic: 77, volume: null} +- model: comics.issue + pk: 219 + fields: {number: '8', is_read: false, in_stock: false, comic: 77, volume: null} +- model: comics.issue + pk: 220 + fields: {number: '1', is_read: false, in_stock: false, comic: 91, volume: null} +- model: comics.issue + pk: 221 + fields: {number: '2', is_read: false, in_stock: false, comic: 91, volume: null} +- model: comics.issue + pk: 222 + fields: {number: '3', is_read: false, in_stock: false, comic: 91, volume: null} +- model: comics.issue + pk: 223 + fields: {number: '4', is_read: false, in_stock: false, comic: 91, volume: null} +- model: comics.issue + pk: 224 + fields: {number: '5', is_read: false, in_stock: false, comic: 91, volume: null} +- model: comics.issue + pk: 225 + fields: {number: '6', is_read: false, in_stock: false, comic: 91, volume: null} +- model: comics.issue + pk: 226 + fields: {number: '7', is_read: false, in_stock: false, comic: 91, volume: null} +- model: comics.issue + pk: 227 + fields: {number: '8', is_read: false, in_stock: false, comic: 91, volume: null} +- model: comics.issue + pk: 228 + fields: {number: '9', is_read: false, in_stock: false, comic: 91, volume: null} +- model: comics.issue + pk: 229 + fields: {number: '10', is_read: false, in_stock: false, comic: 91, volume: null} +- model: comics.issue + pk: 230 + fields: {number: '11', is_read: false, in_stock: false, comic: 91, volume: null} +- model: comics.issue + pk: 231 + fields: {number: '12', is_read: false, in_stock: false, comic: 91, volume: null} +- model: comics.issue + pk: 232 + fields: {number: '1', is_read: false, in_stock: false, comic: 92, volume: null} +- model: comics.issue + pk: 233 + fields: {number: '1', is_read: false, in_stock: false, comic: 93, volume: null} +- model: comics.issue + pk: 234 + fields: {number: '2', is_read: false, in_stock: false, comic: 93, volume: null} +- model: comics.issue + pk: 235 + fields: {number: '3', is_read: false, in_stock: false, comic: 93, volume: null} +- model: comics.issue + pk: 236 + fields: {number: '1', is_read: false, in_stock: false, comic: 96, volume: null} +- model: comics.issue + pk: 237 + fields: {number: '2', is_read: false, in_stock: false, comic: 96, volume: null} +- model: comics.issue + pk: 238 + fields: {number: '3', is_read: false, in_stock: false, comic: 96, volume: null} +- model: comics.issue + pk: 239 + fields: {number: '4', is_read: false, in_stock: false, comic: 96, volume: null} +- model: comics.issue + pk: 240 + fields: {number: '5', is_read: false, in_stock: false, comic: 96, volume: null} +- model: comics.issue + pk: 241 + fields: {number: '6', is_read: false, in_stock: false, comic: 96, volume: null} +- model: comics.issue + pk: 242 + fields: {number: '7', is_read: false, in_stock: false, comic: 96, volume: null} +- model: comics.issue + pk: 243 + fields: {number: '8', is_read: false, in_stock: false, comic: 96, volume: null} +- model: comics.issue + pk: 244 + fields: {number: '9', is_read: false, in_stock: false, comic: 96, volume: null} +- model: comics.issue + pk: 245 + fields: {number: '10', is_read: false, in_stock: false, comic: 96, volume: null} +- model: comics.issue + pk: 246 + fields: {number: '11', is_read: false, in_stock: false, comic: 96, volume: null} +- model: comics.issue + pk: 247 + fields: {number: '12', is_read: false, in_stock: false, comic: 96, volume: null} +- model: comics.issue + pk: 248 + fields: {number: '1', is_read: false, in_stock: false, comic: 103, volume: null} +- model: comics.issue + pk: 249 + fields: {number: '2', is_read: false, in_stock: false, comic: 103, volume: null} +- model: comics.issue + pk: 250 + fields: {number: '3', is_read: false, in_stock: false, comic: 103, volume: null} +- model: comics.issue + pk: 251 + fields: {number: '4', is_read: false, in_stock: false, comic: 103, volume: null} +- model: comics.issue + pk: 252 + fields: {number: '1', is_read: false, in_stock: false, comic: 107, volume: null} +- model: comics.issue + pk: 253 + fields: {number: '2', is_read: false, in_stock: false, comic: 107, volume: null} +- model: comics.issue + pk: 254 + fields: {number: '3', is_read: false, in_stock: false, comic: 107, volume: null} +- model: comics.issue + pk: 255 + fields: {number: '4', is_read: false, in_stock: false, comic: 107, volume: null} +- model: comics.issue + pk: 256 + fields: {number: '5', is_read: false, in_stock: false, comic: 107, volume: null} +- model: comics.issue + pk: 257 + fields: {number: '6', is_read: false, in_stock: false, comic: 107, volume: null} +- model: comics.issue + pk: 258 + fields: {number: '7', is_read: false, in_stock: false, comic: 107, volume: null} +- model: comics.issue + pk: 259 + fields: {number: '8', is_read: false, in_stock: false, comic: 107, volume: null} +- model: comics.issue + pk: 260 + fields: {number: '1', is_read: false, in_stock: false, comic: 133, volume: null} +- model: comics.issue + pk: 261 + fields: {number: '2', is_read: false, in_stock: false, comic: 133, volume: null} +- model: comics.issue + pk: 262 + fields: {number: '3', is_read: false, in_stock: false, comic: 133, volume: null} +- model: comics.issue + pk: 263 + fields: {number: '4', is_read: false, in_stock: false, comic: 133, volume: null} +- model: comics.issue + pk: 264 + fields: {number: '5', is_read: false, in_stock: false, comic: 133, volume: null} +- model: comics.issue + pk: 265 + fields: {number: '6', is_read: false, in_stock: false, comic: 133, volume: null} +- model: comics.issue + pk: 266 + fields: {number: '1', is_read: false, in_stock: false, comic: 138, volume: null} +- model: comics.issue + pk: 267 + fields: {number: '2', is_read: false, in_stock: false, comic: 138, volume: null} +- model: comics.issue + pk: 268 + fields: {number: '3', is_read: false, in_stock: false, comic: 138, volume: null} +- model: comics.issue + pk: 269 + fields: {number: '4', is_read: false, in_stock: false, comic: 138, volume: null} +- model: comics.issue + pk: 270 + fields: {number: '5', is_read: false, in_stock: false, comic: 138, volume: null} +- model: comics.issue + pk: 271 + fields: {number: '6', is_read: false, in_stock: false, comic: 138, volume: null} +- model: comics.issue + pk: 272 + fields: {number: '1', is_read: false, in_stock: false, comic: 139, volume: null} +- model: comics.issue + pk: 273 + fields: {number: '1', is_read: false, in_stock: false, comic: 140, volume: null} +- model: comics.issue + pk: 274 + fields: {number: '19', is_read: false, in_stock: false, comic: 137, volume: null} +- model: comics.issue + pk: 275 + fields: {number: '1', is_read: false, in_stock: false, comic: 143, volume: null} +- model: comics.issue + pk: 276 + fields: {number: '2', is_read: false, in_stock: false, comic: 143, volume: null} +- model: comics.issue + pk: 277 + fields: {number: '3', is_read: false, in_stock: false, comic: 143, volume: null} +- model: comics.issue + pk: 278 + fields: {number: '4', is_read: false, in_stock: false, comic: 143, volume: null} +- model: comics.issue + pk: 279 + fields: {number: '5', is_read: false, in_stock: false, comic: 143, volume: null} +- model: comics.issue + pk: 280 + fields: {number: '6', is_read: false, in_stock: false, comic: 143, volume: null} +- model: comics.issue + pk: 281 + fields: {number: '1', is_read: false, in_stock: false, comic: 144, volume: null} +- model: comics.issue + pk: 282 + fields: {number: '1', is_read: false, in_stock: false, comic: 145, volume: null} +- model: comics.issue + pk: 283 + fields: {number: '2', is_read: false, in_stock: false, comic: 145, volume: null} +- model: comics.issue + pk: 284 + fields: {number: '3', is_read: false, in_stock: false, comic: 145, volume: null} +- model: comics.issue + pk: 285 + fields: {number: '1', is_read: false, in_stock: false, comic: 146, volume: null} +- model: comics.issue + pk: 286 + fields: {number: '2', is_read: false, in_stock: false, comic: 146, volume: null} +- model: comics.issue + pk: 287 + fields: {number: '3', is_read: false, in_stock: false, comic: 146, volume: null} +- model: comics.issue + pk: 288 + fields: {number: '4', is_read: false, in_stock: false, comic: 146, volume: null} +- model: comics.issue + pk: 289 + fields: {number: '5', is_read: false, in_stock: false, comic: 146, volume: null} +- model: comics.issue + pk: 290 + fields: {number: '6', is_read: false, in_stock: false, comic: 146, volume: null} +- model: comics.issue + pk: 291 + fields: {number: '46', is_read: false, in_stock: false, comic: 151, volume: null} +- model: comics.issue + pk: 292 + fields: {number: '1', is_read: false, in_stock: false, comic: 149, volume: null} +- model: comics.issue + pk: 293 + fields: {number: '2', is_read: false, in_stock: false, comic: 149, volume: null} +- model: comics.issue + pk: 294 + fields: {number: '3', is_read: false, in_stock: false, comic: 149, volume: null} +- model: comics.issue + pk: 295 + fields: {number: '4', is_read: false, in_stock: false, comic: 149, volume: null} +- model: comics.issue + pk: 296 + fields: {number: '5', is_read: false, in_stock: false, comic: 149, volume: null} +- model: comics.issue + pk: 297 + fields: {number: '1', is_read: false, in_stock: false, comic: 142, volume: null} +- model: comics.issue + pk: 298 + fields: {number: '1', is_read: false, in_stock: false, comic: 148, volume: null} +- model: comics.issue + pk: 299 + fields: {number: '1', is_read: false, in_stock: false, comic: 147, volume: null} +- model: comics.issue + pk: 300 + fields: {number: '2', is_read: false, in_stock: false, comic: 147, volume: null} +- model: comics.issue + pk: 301 + fields: {number: '3', is_read: false, in_stock: false, comic: 147, volume: null} +- model: comics.issue + pk: 302 + fields: {number: '4', is_read: false, in_stock: false, comic: 147, volume: null} +- model: comics.issue + pk: 303 + fields: {number: '5', is_read: false, in_stock: false, comic: 147, volume: null} +- model: comics.issue + pk: 304 + fields: {number: '6', is_read: false, in_stock: false, comic: 147, volume: null} +- model: comics.issue + pk: 305 + fields: {number: '1', is_read: false, in_stock: false, comic: 129, volume: null} +- model: comics.issue + pk: 306 + fields: {number: '2', is_read: false, in_stock: false, comic: 129, volume: null} +- model: comics.issue + pk: 307 + fields: {number: '3', is_read: false, in_stock: false, comic: 129, volume: null} +- model: comics.issue + pk: 308 + fields: {number: '4', is_read: false, in_stock: false, comic: 129, volume: null} +- model: comics.issue + pk: 309 + fields: {number: '5', is_read: false, in_stock: false, comic: 129, volume: null} +- model: comics.issue + pk: 310 + fields: {number: '6', is_read: false, in_stock: false, comic: 129, volume: null} +- model: comics.issue + pk: 311 + fields: {number: '7', is_read: false, in_stock: false, comic: 129, volume: null} +- model: comics.issue + pk: 312 + fields: {number: '8', is_read: false, in_stock: false, comic: 129, volume: null} +- model: comics.issue + pk: 313 + fields: {number: '9', is_read: false, in_stock: false, comic: 129, volume: null} +- model: comics.issue + pk: 314 + fields: {number: '10', is_read: false, in_stock: false, comic: 129, volume: null} +- model: comics.issue + pk: 315 + fields: {number: '11', is_read: false, in_stock: false, comic: 129, volume: null} +- model: comics.issue + pk: 316 + fields: {number: '12', is_read: false, in_stock: false, comic: 129, volume: null} +- model: comics.issue + pk: 317 + fields: {number: '1', is_read: false, in_stock: false, comic: 119, volume: null} +- model: comics.issue + pk: 318 + fields: {number: '2', is_read: false, in_stock: false, comic: 119, volume: null} +- model: comics.issue + pk: 319 + fields: {number: '3', is_read: false, in_stock: false, comic: 119, volume: null} +- model: comics.issue + pk: 320 + fields: {number: '4', is_read: false, in_stock: false, comic: 119, volume: null} +- model: comics.issue + pk: 321 + fields: {number: '5', is_read: false, in_stock: false, comic: 119, volume: null} +- model: comics.issue + pk: 322 + fields: {number: '6', is_read: false, in_stock: false, comic: 119, volume: null} +- model: comics.issue + pk: 323 + fields: {number: '0', is_read: false, in_stock: false, comic: 120, volume: null} +- model: comics.issue + pk: 324 + fields: {number: '1', is_read: false, in_stock: false, comic: 120, volume: null} +- model: comics.issue + pk: 325 + fields: {number: '2', is_read: false, in_stock: false, comic: 120, volume: null} +- model: comics.issue + pk: 326 + fields: {number: '3', is_read: false, in_stock: false, comic: 120, volume: null} +- model: comics.issue + pk: 327 + fields: {number: '4', is_read: false, in_stock: false, comic: 120, volume: null} +- model: comics.issue + pk: 328 + fields: {number: '1', is_read: false, in_stock: false, comic: 128, volume: null} +- model: comics.issue + pk: 329 + fields: {number: '2', is_read: false, in_stock: false, comic: 128, volume: null} +- model: comics.issue + pk: 330 + fields: {number: '1', is_read: false, in_stock: false, comic: 118, volume: null} +- model: comics.issue + pk: 331 + fields: {number: '2', is_read: false, in_stock: false, comic: 118, volume: null} +- model: comics.issue + pk: 332 + fields: {number: '3', is_read: false, in_stock: false, comic: 118, volume: null} +- model: comics.issue + pk: 333 + fields: {number: '207', is_read: false, in_stock: false, comic: 121, volume: null} +- model: comics.issue + pk: 334 + fields: {number: '208', is_read: false, in_stock: false, comic: 121, volume: null} +- model: comics.issue + pk: 335 + fields: {number: '209', is_read: false, in_stock: false, comic: 121, volume: null} +- model: comics.issue + pk: 336 + fields: {number: '210', is_read: false, in_stock: false, comic: 121, volume: null} +- model: comics.issue + pk: 337 + fields: {number: '211', is_read: false, in_stock: false, comic: 121, volume: null} +- model: comics.issue + pk: 338 + fields: {number: '212', is_read: false, in_stock: false, comic: 121, volume: null} +- model: comics.issue + pk: 339 + fields: {number: '213', is_read: false, in_stock: false, comic: 121, volume: null} +- model: comics.issue + pk: 340 + fields: {number: '214', is_read: false, in_stock: false, comic: 121, volume: null} +- model: comics.issue + pk: 341 + fields: {number: '215', is_read: false, in_stock: false, comic: 121, volume: null} +- model: comics.issue + pk: 342 + fields: {number: '17', is_read: false, in_stock: false, comic: 122, volume: null} +- model: comics.issue + pk: 343 + fields: {number: '18', is_read: false, in_stock: false, comic: 122, volume: null} +- model: comics.issue + pk: 344 + fields: {number: '19', is_read: false, in_stock: false, comic: 122, volume: null} +- model: comics.issue + pk: 345 + fields: {number: '20', is_read: false, in_stock: false, comic: 122, volume: null} +- model: comics.issue + pk: 346 + fields: {number: '21', is_read: false, in_stock: false, comic: 122, volume: null} +- model: comics.issue + pk: 347 + fields: {number: '22', is_read: false, in_stock: false, comic: 122, volume: null} +- model: comics.issue + pk: 348 + fields: {number: '1', is_read: false, in_stock: false, comic: 132, volume: null} +- model: comics.issue + pk: 349 + fields: {number: '48', is_read: false, in_stock: false, comic: 131, volume: null} +- model: comics.issue + pk: 350 + fields: {number: '49', is_read: false, in_stock: false, comic: 131, volume: null} +- model: comics.issue + pk: 351 + fields: {number: '50', is_read: false, in_stock: false, comic: 131, volume: null} +- model: comics.issue + pk: 352 + fields: {number: '1', is_read: false, in_stock: false, comic: 130, volume: null} +- model: comics.issue + pk: 353 + fields: {number: '1', is_read: false, in_stock: false, comic: 127, volume: null} +- model: comics.issue + pk: 354 + fields: {number: '1', is_read: false, in_stock: false, comic: 126, volume: null} +- model: comics.issue + pk: 355 + fields: {number: '1', is_read: false, in_stock: false, comic: 125, volume: null} +- model: comics.issue + pk: 356 + fields: {number: '1', is_read: false, in_stock: false, comic: 124, volume: null} +- model: comics.issue + pk: 357 + fields: {number: '81', is_read: false, in_stock: false, comic: 141, volume: null} +- model: comics.issue + pk: 358 + fields: {number: '82', is_read: false, in_stock: false, comic: 141, volume: null} +- model: comics.issue + pk: 359 + fields: {number: '83', is_read: false, in_stock: false, comic: 141, volume: null} +- model: comics.issue + pk: 360 + fields: {number: '84', is_read: false, in_stock: false, comic: 141, volume: null} +- model: comics.issue + pk: 361 + fields: {number: '85', is_read: false, in_stock: false, comic: 141, volume: null} +- model: comics.issue + pk: 362 + fields: {number: '86', is_read: false, in_stock: false, comic: 141, volume: null} +- model: comics.issue + pk: 363 + fields: {number: '87', is_read: false, in_stock: false, comic: 141, volume: null} +- model: comics.issue + pk: 364 + fields: {number: '88', is_read: false, in_stock: false, comic: 141, volume: null} +- model: comics.issue + pk: 365 + fields: {number: '89', is_read: false, in_stock: false, comic: 141, volume: null} +- model: comics.issue + pk: 366 + fields: {number: '90', is_read: false, in_stock: false, comic: 141, volume: null} +- model: comics.issue + pk: 367 + fields: {number: '91', is_read: false, in_stock: false, comic: 141, volume: null} +- model: comics.issue + pk: 368 + fields: {number: '92', is_read: false, in_stock: false, comic: 141, volume: null} +- model: comics.issue + pk: 369 + fields: {number: '19', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 370 + fields: {number: '20', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 371 + fields: {number: '21', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 372 + fields: {number: '22', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 373 + fields: {number: '23', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 374 + fields: {number: '24', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 375 + fields: {number: '25', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 376 + fields: {number: '26', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 377 + fields: {number: '27', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 378 + fields: {number: '28', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 379 + fields: {number: '29', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 380 + fields: {number: '30', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 381 + fields: {number: '31', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 382 + fields: {number: '32', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 383 + fields: {number: '33', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 384 + fields: {number: '34', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 385 + fields: {number: '35', is_read: false, in_stock: false, comic: 123, volume: null} +- model: comics.issue + pk: 386 + fields: {number: '1', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 387 + fields: {number: '2', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 388 + fields: {number: '3', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 389 + fields: {number: '4', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 390 + fields: {number: '5', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 391 + fields: {number: '6', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 392 + fields: {number: '7', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 393 + fields: {number: '8', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 394 + fields: {number: '9', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 395 + fields: {number: '10', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 396 + fields: {number: '11', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 397 + fields: {number: '12', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 398 + fields: {number: '13', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 399 + fields: {number: '14', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 400 + fields: {number: '15', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 401 + fields: {number: '16', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 402 + fields: {number: '17', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 403 + fields: {number: '18', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 404 + fields: {number: '19', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 405 + fields: {number: '20', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 406 + fields: {number: '21', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 407 + fields: {number: '22', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 408 + fields: {number: '23', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 409 + fields: {number: '24', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 410 + fields: {number: '25', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 411 + fields: {number: '26', is_read: false, in_stock: false, comic: 110, volume: null} +- model: comics.issue + pk: 412 + fields: {number: '1', is_read: false, in_stock: false, comic: 108, volume: null} +- model: comics.issue + pk: 413 + fields: {number: '2', is_read: false, in_stock: false, comic: 108, volume: null} +- model: comics.issue + pk: 414 + fields: {number: '3', is_read: false, in_stock: false, comic: 108, volume: null} +- model: comics.issue + pk: 415 + fields: {number: '4', is_read: false, in_stock: false, comic: 108, volume: null} +- model: comics.issue + pk: 416 + fields: {number: '5', is_read: false, in_stock: false, comic: 108, volume: null} +- model: comics.issue + pk: 417 + fields: {number: '1', is_read: false, in_stock: false, comic: 109, volume: null} +- model: comics.issue + pk: 418 + fields: {number: '2', is_read: false, in_stock: false, comic: 109, volume: null} +- model: comics.issue + pk: 419 + fields: {number: '3', is_read: false, in_stock: false, comic: 109, volume: null} +- model: comics.issue + pk: 420 + fields: {number: '4', is_read: false, in_stock: false, comic: 109, volume: null} +- model: comics.issue + pk: 421 + fields: {number: '1', is_read: false, in_stock: false, comic: 111, volume: null} +- model: comics.issue + pk: 422 + fields: {number: '2', is_read: false, in_stock: false, comic: 111, volume: null} +- model: comics.issue + pk: 423 + fields: {number: '3', is_read: false, in_stock: false, comic: 111, volume: null} +- model: comics.issue + pk: 424 + fields: {number: '4', is_read: false, in_stock: false, comic: 111, volume: null} +- model: comics.issue + pk: 425 + fields: {number: '5', is_read: false, in_stock: false, comic: 111, volume: null} +- model: comics.issue + pk: 426 + fields: {number: '6', is_read: false, in_stock: false, comic: 111, volume: null} +- model: comics.issue + pk: 427 + fields: {number: '1', is_read: false, in_stock: false, comic: 113, volume: null} +- model: comics.issue + pk: 428 + fields: {number: '1', is_read: false, in_stock: false, comic: 112, volume: null} +- model: comics.issue + pk: 429 + fields: {number: '2', is_read: false, in_stock: false, comic: 112, volume: null} +- model: comics.issue + pk: 430 + fields: {number: '3', is_read: false, in_stock: false, comic: 112, volume: null} +- model: comics.issue + pk: 431 + fields: {number: '4', is_read: false, in_stock: false, comic: 112, volume: null} +- model: comics.issue + pk: 432 + fields: {number: '1', is_read: false, in_stock: false, comic: 114, volume: null} +- model: comics.issue + pk: 433 + fields: {number: '2', is_read: false, in_stock: false, comic: 114, volume: null} +- model: comics.issue + pk: 434 + fields: {number: '3', is_read: false, in_stock: false, comic: 114, volume: null} +- model: comics.issue + pk: 435 + fields: {number: '4', is_read: false, in_stock: false, comic: 114, volume: null} +- model: comics.issue + pk: 436 + fields: {number: '5', is_read: false, in_stock: false, comic: 114, volume: null} +- model: comics.issue + pk: 437 + fields: {number: '1', is_read: false, in_stock: false, comic: 115, volume: null} +- model: comics.issue + pk: 438 + fields: {number: '2', is_read: false, in_stock: false, comic: 115, volume: null} +- model: comics.issue + pk: 439 + fields: {number: '3', is_read: false, in_stock: false, comic: 115, volume: null} +- model: comics.issue + pk: 440 + fields: {number: '4', is_read: false, in_stock: false, comic: 115, volume: null} +- model: comics.issue + pk: 441 + fields: {number: '5', is_read: false, in_stock: false, comic: 115, volume: null} +- model: comics.issue + pk: 442 + fields: {number: '1', is_read: false, in_stock: false, comic: 116, volume: null} +- model: comics.issue + pk: 443 + fields: {number: '2', is_read: false, in_stock: false, comic: 116, volume: null} +- model: comics.issue + pk: 444 + fields: {number: '3', is_read: false, in_stock: false, comic: 116, volume: null} +- model: comics.issue + pk: 445 + fields: {number: '4', is_read: false, in_stock: false, comic: 116, volume: null} +- model: comics.issue + pk: 446 + fields: {number: '13', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 447 + fields: {number: '14', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 448 + fields: {number: '15', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 449 + fields: {number: '16', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 450 + fields: {number: '17', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 451 + fields: {number: '18', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 452 + fields: {number: '19', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 453 + fields: {number: '20', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 454 + fields: {number: '21', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 455 + fields: {number: '22', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 456 + fields: {number: '23', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 457 + fields: {number: '24', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 458 + fields: {number: '25', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 459 + fields: {number: '26', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 460 + fields: {number: '27', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 461 + fields: {number: '28', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 462 + fields: {number: '29', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 463 + fields: {number: '30', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 464 + fields: {number: '31', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 465 + fields: {number: '32', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 466 + fields: {number: '33', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 467 + fields: {number: '34', is_read: false, in_stock: false, comic: 106, volume: null} +- model: comics.issue + pk: 468 + fields: {number: '1', is_read: false, in_stock: false, comic: 104, volume: null} +- model: comics.issue + pk: 469 + fields: {number: '2', is_read: false, in_stock: false, comic: 104, volume: null} +- model: comics.issue + pk: 470 + fields: {number: '1', is_read: false, in_stock: false, comic: 100, volume: null} +- model: comics.issue + pk: 471 + fields: {number: '2', is_read: false, in_stock: false, comic: 100, volume: null} +- model: comics.issue + pk: 472 + fields: {number: '3', is_read: false, in_stock: false, comic: 100, volume: null} +- model: comics.issue + pk: 473 + fields: {number: '4', is_read: false, in_stock: false, comic: 100, volume: null} +- model: comics.issue + pk: 474 + fields: {number: '5', is_read: false, in_stock: false, comic: 100, volume: null} +- model: comics.issue + pk: 475 + fields: {number: '6', is_read: false, in_stock: false, comic: 100, volume: null} +- model: comics.issue + pk: 476 + fields: {number: '7', is_read: false, in_stock: false, comic: 100, volume: null} +- model: comics.issue + pk: 477 + fields: {number: '100', is_read: false, in_stock: false, comic: 105, volume: null} +- model: comics.issue + pk: 478 + fields: {number: '101', is_read: false, in_stock: false, comic: 105, volume: null} +- model: comics.issue + pk: 479 + fields: {number: '102', is_read: false, in_stock: false, comic: 105, volume: null} +- model: comics.issue + pk: 480 + fields: {number: '103', is_read: false, in_stock: false, comic: 105, volume: null} +- model: comics.issue + pk: 481 + fields: {number: '104', is_read: false, in_stock: false, comic: 105, volume: null} +- model: comics.issue + pk: 482 + fields: {number: '105', is_read: false, in_stock: false, comic: 105, volume: null} +- model: comics.issue + pk: 483 + fields: {number: '106', is_read: false, in_stock: false, comic: 105, volume: null} +- model: comics.issue + pk: 484 + fields: {number: '107', is_read: false, in_stock: false, comic: 105, volume: null} +- model: comics.issue + pk: 485 + fields: {number: '108', is_read: false, in_stock: false, comic: 105, volume: null} +- model: comics.issue + pk: 486 + fields: {number: '109', is_read: false, in_stock: false, comic: 105, volume: null} +- model: comics.issue + pk: 487 + fields: {number: '110', is_read: false, in_stock: false, comic: 105, volume: null} +- model: comics.issue + pk: 488 + fields: {number: '111', is_read: false, in_stock: false, comic: 105, volume: null} +- model: comics.issue + pk: 489 + fields: {number: '112', is_read: false, in_stock: false, comic: 105, volume: null} +- model: comics.issue + pk: 490 + fields: {number: '1', is_read: false, in_stock: false, comic: 90, volume: null} +- model: comics.issue + pk: 491 + fields: {number: '2', is_read: false, in_stock: false, comic: 90, volume: null} +- model: comics.issue + pk: 492 + fields: {number: '3', is_read: false, in_stock: false, comic: 90, volume: null} +- model: comics.issue + pk: 493 + fields: {number: '4', is_read: false, in_stock: false, comic: 90, volume: null} +- model: comics.issue + pk: 494 + fields: {number: '1', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 495 + fields: {number: '2', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 496 + fields: {number: '3', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 497 + fields: {number: '4', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 498 + fields: {number: '5', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 499 + fields: {number: '6', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 500 + fields: {number: '7', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 501 + fields: {number: '8', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 502 + fields: {number: '9', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 503 + fields: {number: '10', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 504 + fields: {number: '11', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 505 + fields: {number: '12', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 506 + fields: {number: '13', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 507 + fields: {number: '14', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 508 + fields: {number: '15', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 509 + fields: {number: '16', is_read: false, in_stock: false, comic: 89, volume: null} +- model: comics.issue + pk: 510 + fields: {number: '151', is_read: false, in_stock: false, comic: 88, volume: null} +- model: comics.issue + pk: 511 + fields: {number: '152', is_read: false, in_stock: false, comic: 88, volume: null} +- model: comics.issue + pk: 512 + fields: {number: '153', is_read: false, in_stock: false, comic: 88, volume: null} +- model: comics.issue + pk: 513 + fields: {number: '154', is_read: false, in_stock: false, comic: 88, volume: null} +- model: comics.issue + pk: 514 + fields: {number: '1', is_read: false, in_stock: false, comic: 94, volume: null} +- model: comics.issue + pk: 515 + fields: {number: '2', is_read: false, in_stock: false, comic: 94, volume: null} +- model: comics.issue + pk: 516 + fields: {number: '3', is_read: false, in_stock: false, comic: 94, volume: null} +- model: comics.issue + pk: 517 + fields: {number: '1', is_read: false, in_stock: false, comic: 95, volume: null} +- model: comics.issue + pk: 518 + fields: {number: '2', is_read: false, in_stock: false, comic: 95, volume: null} +- model: comics.issue + pk: 519 + fields: {number: '3', is_read: false, in_stock: false, comic: 95, volume: null} +- model: comics.issue + pk: 520 + fields: {number: '4', is_read: false, in_stock: false, comic: 95, volume: null} +- model: comics.issue + pk: 521 + fields: {number: '1', is_read: false, in_stock: false, comic: 68, volume: null} +- model: comics.issue + pk: 522 + fields: {number: '2', is_read: false, in_stock: false, comic: 68, volume: null} +- model: comics.issue + pk: 523 + fields: {number: '3', is_read: false, in_stock: false, comic: 68, volume: null} +- model: comics.issue + pk: 524 + fields: {number: '4', is_read: false, in_stock: false, comic: 68, volume: null} +- model: comics.issue + pk: 525 + fields: {number: '5', is_read: false, in_stock: false, comic: 68, volume: null} +- model: comics.issue + pk: 526 + fields: {number: '1', is_read: false, in_stock: false, comic: 70, volume: null} +- model: comics.issue + pk: 527 + fields: {number: '2', is_read: false, in_stock: false, comic: 70, volume: null} +- model: comics.issue + pk: 528 + fields: {number: '3', is_read: false, in_stock: false, comic: 70, volume: null} +- model: comics.issue + pk: 529 + fields: {number: '4', is_read: false, in_stock: false, comic: 70, volume: null} +- model: comics.issue + pk: 530 + fields: {number: '1', is_read: false, in_stock: false, comic: 71, volume: null} +- model: comics.issue + pk: 531 + fields: {number: '2', is_read: false, in_stock: false, comic: 71, volume: null} +- model: comics.issue + pk: 532 + fields: {number: '3', is_read: false, in_stock: false, comic: 71, volume: null} +- model: comics.issue + pk: 533 + fields: {number: '1', is_read: false, in_stock: false, comic: 69, volume: null} +- model: comics.issue + pk: 534 + fields: {number: '2', is_read: false, in_stock: false, comic: 69, volume: null} +- model: comics.issue + pk: 535 + fields: {number: '1', is_read: false, in_stock: false, comic: 67, volume: null} +- model: comics.issue + pk: 536 + fields: {number: '2', is_read: false, in_stock: false, comic: 67, volume: null} +- model: comics.issue + pk: 537 + fields: {number: '3', is_read: false, in_stock: false, comic: 67, volume: null} +- model: comics.issue + pk: 538 + fields: {number: '1', is_read: false, in_stock: false, comic: 53, volume: null} +- model: comics.issue + pk: 539 + fields: {number: '2', is_read: false, in_stock: false, comic: 53, volume: null} +- model: comics.issue + pk: 540 + fields: {number: '3', is_read: false, in_stock: false, comic: 53, volume: null} +- model: comics.issue + pk: 541 + fields: {number: '1', is_read: false, in_stock: false, comic: 57, volume: null} +- model: comics.issue + pk: 542 + fields: {number: '2', is_read: false, in_stock: false, comic: 57, volume: null} +- model: comics.issue + pk: 543 + fields: {number: '3', is_read: false, in_stock: false, comic: 57, volume: null} +- model: comics.issue + pk: 544 + fields: {number: '4', is_read: false, in_stock: false, comic: 57, volume: null} +- model: comics.issue + pk: 545 + fields: {number: '5', is_read: false, in_stock: false, comic: 57, volume: null} +- model: comics.issue + pk: 546 + fields: {number: '6', is_read: false, in_stock: false, comic: 57, volume: null} +- model: comics.issue + pk: 547 + fields: {number: '7', is_read: false, in_stock: false, comic: 57, volume: null} +- model: comics.issue + pk: 548 + fields: {number: '8', is_read: false, in_stock: false, comic: 57, volume: null} +- model: comics.issue + pk: 549 + fields: {number: '9', is_read: false, in_stock: false, comic: 57, volume: null} +- model: comics.issue + pk: 550 + fields: {number: '10', is_read: false, in_stock: false, comic: 57, volume: null} +- model: comics.issue + pk: 551 + fields: {number: '11', is_read: false, in_stock: false, comic: 57, volume: null} +- model: comics.issue + pk: 552 + fields: {number: '12', is_read: false, in_stock: false, comic: 57, volume: null} +- model: comics.issue + pk: 553 + fields: {number: '13', is_read: false, in_stock: false, comic: 57, volume: null} +- model: comics.issue + pk: 554 + fields: {number: 1/2, is_read: false, in_stock: false, comic: 41, volume: null} +- model: comics.issue + pk: 555 + fields: {number: '1', is_read: false, in_stock: false, comic: 41, volume: null} +- model: comics.issue + pk: 556 + fields: {number: '2', is_read: false, in_stock: false, comic: 41, volume: null} +- model: comics.issue + pk: 557 + fields: {number: '3', is_read: false, in_stock: false, comic: 41, volume: null} +- model: comics.issue + pk: 558 + fields: {number: '4', is_read: false, in_stock: false, comic: 41, volume: null} +- model: comics.issue + pk: 559 + fields: {number: '5', is_read: false, in_stock: false, comic: 41, volume: null} +- model: comics.issue + pk: 560 + fields: {number: '6', is_read: false, in_stock: false, comic: 41, volume: null} +- model: comics.issue + pk: 561 + fields: {number: '7', is_read: false, in_stock: false, comic: 41, volume: null} +- model: comics.issue + pk: 562 + fields: {number: '8', is_read: false, in_stock: false, comic: 41, volume: null} +- model: comics.issue + pk: 563 + fields: {number: '9', is_read: false, in_stock: false, comic: 41, volume: null} +- model: comics.issue + pk: 564 + fields: {number: '10', is_read: false, in_stock: false, comic: 41, volume: null} +- model: comics.issue + pk: 565 + fields: {number: '11', is_read: false, in_stock: false, comic: 41, volume: null} +- model: comics.issue + pk: 566 + fields: {number: '12', is_read: false, in_stock: false, comic: 41, volume: null} +- model: comics.issue + pk: 567 + fields: {number: '13', is_read: false, in_stock: false, comic: 41, volume: null} +- model: comics.issue + pk: 568 + fields: {number: '14', is_read: false, in_stock: false, comic: 41, volume: null} +- model: comics.issue + pk: 569 + fields: {number: '1', is_read: false, in_stock: false, comic: 42, volume: null} +- model: comics.issue + pk: 570 + fields: {number: '1', is_read: false, in_stock: false, comic: 43, volume: null} +- model: comics.issue + pk: 571 + fields: {number: '2', is_read: false, in_stock: false, comic: 43, volume: null} +- model: comics.issue + pk: 572 + fields: {number: '3', is_read: false, in_stock: false, comic: 43, volume: null} +- model: comics.issue + pk: 573 + fields: {number: '1', is_read: false, in_stock: false, comic: 44, volume: null} +- model: comics.issue + pk: 574 + fields: {number: '1', is_read: false, in_stock: false, comic: 45, volume: null} +- model: comics.issue + pk: 575 + fields: {number: '2', is_read: false, in_stock: false, comic: 45, volume: null} +- model: comics.issue + pk: 576 + fields: {number: '3', is_read: false, in_stock: false, comic: 45, volume: null} +- model: comics.issue + pk: 577 + fields: {number: '1', is_read: false, in_stock: false, comic: 46, volume: null} +- model: comics.issue + pk: 578 + fields: {number: '1', is_read: false, in_stock: false, comic: 47, volume: null} +- model: comics.issue + pk: 579 + fields: {number: '1', is_read: false, in_stock: false, comic: 48, volume: null} +- model: comics.issue + pk: 580 + fields: {number: '0', is_read: false, in_stock: false, comic: 49, volume: null} +- model: comics.issue + pk: 581 + fields: {number: '1', is_read: false, in_stock: false, comic: 49, volume: null} +- model: comics.issue + pk: 582 + fields: {number: '2', is_read: false, in_stock: false, comic: 49, volume: null} +- model: comics.issue + pk: 583 + fields: {number: '3', is_read: false, in_stock: false, comic: 49, volume: null} +- model: comics.issue + pk: 584 + fields: {number: '4', is_read: false, in_stock: false, comic: 49, volume: null} +- model: comics.issue + pk: 585 + fields: {number: '5', is_read: false, in_stock: false, comic: 49, volume: null} +- model: comics.issue + pk: 586 + fields: {number: '1', is_read: false, in_stock: false, comic: 51, volume: null} +- model: comics.issue + pk: 587 + fields: {number: '1', is_read: false, in_stock: false, comic: 52, volume: null} +- model: comics.issue + pk: 588 + fields: {number: '1', is_read: false, in_stock: false, comic: 27, volume: null} +- model: comics.issue + pk: 589 + fields: {number: '2', is_read: false, in_stock: false, comic: 27, volume: null} +- model: comics.issue + pk: 590 + fields: {number: '3', is_read: false, in_stock: false, comic: 27, volume: null} +- model: comics.issue + pk: 591 + fields: {number: '4', is_read: false, in_stock: false, comic: 27, volume: null} +- model: comics.issue + pk: 592 + fields: {number: '1', is_read: false, in_stock: false, comic: 21, volume: null} +- model: comics.issue + pk: 593 + fields: {number: '2', is_read: false, in_stock: false, comic: 21, volume: null} +- model: comics.issue + pk: 594 + fields: {number: '3', is_read: false, in_stock: false, comic: 21, volume: null} +- model: comics.issue + pk: 595 + fields: {number: '4', is_read: false, in_stock: false, comic: 21, volume: null} +- model: comics.issue + pk: 596 + fields: {number: '5', is_read: false, in_stock: false, comic: 21, volume: null} +- model: comics.issue + pk: 597 + fields: {number: '6', is_read: false, in_stock: false, comic: 21, volume: null} +- model: comics.issue + pk: 598 + fields: {number: '7', is_read: false, in_stock: false, comic: 21, volume: null} +- model: comics.issue + pk: 599 + fields: {number: '8', is_read: false, in_stock: false, comic: 21, volume: null} +- model: comics.issue + pk: 600 + fields: {number: '9', is_read: false, in_stock: false, comic: 21, volume: null} +- model: comics.issue + pk: 601 + fields: {number: '10', is_read: false, in_stock: false, comic: 21, volume: null} +- model: comics.issue + pk: 602 + fields: {number: '11', is_read: false, in_stock: false, comic: 21, volume: null} +- model: comics.issue + pk: 603 + fields: {number: '12', is_read: false, in_stock: false, comic: 21, volume: null} +- model: comics.issue + pk: 604 + fields: {number: '13', is_read: false, in_stock: false, comic: 21, volume: null} +- model: comics.issue + pk: 605 + fields: {number: '14', is_read: false, in_stock: false, comic: 21, volume: null} +- model: comics.issue + pk: 606 + fields: {number: '1', is_read: false, in_stock: false, comic: 10, volume: null} +- model: comics.issue + pk: 607 + fields: {number: '2', is_read: false, in_stock: false, comic: 10, volume: null} +- model: comics.issue + pk: 608 + fields: {number: '3', is_read: false, in_stock: false, comic: 10, volume: null} +- model: comics.issue + pk: 609 + fields: {number: '4', is_read: false, in_stock: false, comic: 10, volume: null} +- model: comics.issue + pk: 610 + fields: {number: '5', is_read: false, in_stock: false, comic: 10, volume: null} +- model: comics.issue + pk: 611 + fields: {number: '6', is_read: false, in_stock: false, comic: 10, volume: null} +- model: comics.issue + pk: 612 + fields: {number: '7', is_read: false, in_stock: false, comic: 10, volume: null} +- model: comics.issue + pk: 613 + fields: {number: '8', is_read: false, in_stock: false, comic: 10, volume: null} +- model: comics.issue + pk: 614 + fields: {number: '9', is_read: false, in_stock: false, comic: 10, volume: null} +- model: comics.issue + pk: 615 + fields: {number: '10', is_read: false, in_stock: false, comic: 10, volume: null} +- model: comics.issue + pk: 616 + fields: {number: '11', is_read: false, in_stock: false, comic: 10, volume: null} +- model: comics.issue + pk: 617 + fields: {number: '12', is_read: false, in_stock: false, comic: 10, volume: null} +- model: comics.issue + pk: 618 + fields: {number: '1', is_read: false, in_stock: false, comic: 13, volume: null} +- model: comics.issue + pk: 619 + fields: {number: '1', is_read: false, in_stock: false, comic: 9, volume: null} +- model: comics.issue + pk: 620 + fields: {number: '2', is_read: false, in_stock: false, comic: 9, volume: null} +- model: comics.issue + pk: 621 + fields: {number: '3', is_read: false, in_stock: false, comic: 9, volume: null} +- model: comics.issue + pk: 622 + fields: {number: '1', is_read: false, in_stock: false, comic: 152, volume: null} +- model: comics.issue + pk: 623 + fields: {number: '2', is_read: false, in_stock: false, comic: 152, volume: null} +- model: comics.issue + pk: 624 + fields: {number: '3', is_read: false, in_stock: false, comic: 152, volume: null} +- model: comics.issue + pk: 625 + fields: {number: '1', is_read: false, in_stock: false, comic: 61, volume: null} +- model: comics.issue + pk: 626 + fields: {number: '2', is_read: false, in_stock: false, comic: 61, volume: null} +- model: comics.issue + pk: 627 + fields: {number: '3', is_read: false, in_stock: false, comic: 61, volume: null} +- model: comics.issue + pk: 628 + fields: {number: '4', is_read: false, in_stock: false, comic: 61, volume: null} +- model: comics.issue + pk: 629 + fields: {number: '1', is_read: false, in_stock: false, comic: 63, volume: null} +- model: comics.issue + pk: 630 + fields: {number: '2', is_read: false, in_stock: false, comic: 63, volume: null} +- model: comics.issue + pk: 631 + fields: {number: '3', is_read: false, in_stock: false, comic: 63, volume: null} +- model: comics.issue + pk: 632 + fields: {number: '0', is_read: false, in_stock: false, comic: 64, volume: null} +- model: comics.issue + pk: 633 + fields: {number: '1', is_read: false, in_stock: false, comic: 64, volume: null} +- model: comics.issue + pk: 634 + fields: {number: '2', is_read: false, in_stock: false, comic: 64, volume: null} +- model: comics.issue + pk: 635 + fields: {number: '3', is_read: false, in_stock: false, comic: 64, volume: null} +- model: comics.issue + pk: 636 + fields: {number: '4', is_read: false, in_stock: false, comic: 64, volume: null} +- model: comics.issue + pk: 637 + fields: {number: '1', is_read: false, in_stock: false, comic: 65, volume: null} +- model: comics.issue + pk: 638 + fields: {number: '1', is_read: false, in_stock: false, comic: 66, volume: null} +- model: comics.issue + pk: 639 + fields: {number: '1', is_read: false, in_stock: false, comic: 72, volume: null} +- model: comics.issue + pk: 640 + fields: {number: '20', is_read: false, in_stock: false, comic: 73, volume: null} +- model: comics.issue + pk: 641 + fields: {number: '39', is_read: false, in_stock: false, comic: 78, volume: null} +- model: comics.issue + pk: 642 + fields: {number: '40', is_read: false, in_stock: false, comic: 78, volume: null} +- model: comics.issue + pk: 643 + fields: {number: '41', is_read: false, in_stock: false, comic: 78, volume: null} +- model: comics.issue + pk: 644 + fields: {number: '42', is_read: false, in_stock: false, comic: 78, volume: null} +- model: comics.issue + pk: 645 + fields: {number: '43', is_read: false, in_stock: false, comic: 78, volume: null} +- model: comics.issue + pk: 646 + fields: {number: '44', is_read: false, in_stock: false, comic: 78, volume: null} +- model: comics.issue + pk: 647 + fields: {number: '1', is_read: false, in_stock: false, comic: 84, volume: null} +- model: comics.issue + pk: 648 + fields: {number: '2', is_read: false, in_stock: false, comic: 84, volume: null} +- model: comics.issue + pk: 649 + fields: {number: '3', is_read: false, in_stock: false, comic: 84, volume: null} +- model: comics.issue + pk: 650 + fields: {number: '1', is_read: false, in_stock: false, comic: 85, volume: null} +- model: comics.issue + pk: 651 + fields: {number: '2', is_read: false, in_stock: false, comic: 85, volume: null} +- model: comics.issue + pk: 652 + fields: {number: '1', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 653 + fields: {number: '2', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 654 + fields: {number: '3', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 655 + fields: {number: '4', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 656 + fields: {number: '5', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 657 + fields: {number: '6', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 658 + fields: {number: '7', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 659 + fields: {number: '8', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 660 + fields: {number: '9', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 661 + fields: {number: '10', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 662 + fields: {number: '11', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 663 + fields: {number: '12', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 664 + fields: {number: '13', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 665 + fields: {number: '14', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 666 + fields: {number: '15', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 667 + fields: {number: '16', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 668 + fields: {number: '17', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 669 + fields: {number: '18', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 670 + fields: {number: '19', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 671 + fields: {number: '20', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 672 + fields: {number: '21', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 673 + fields: {number: '22', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 674 + fields: {number: '23', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 675 + fields: {number: '24', is_read: false, in_stock: false, comic: 83, volume: null} +- model: comics.issue + pk: 676 + fields: {number: '1', is_read: false, in_stock: false, comic: 3, volume: null} +- model: comics.issue + pk: 677 + fields: {number: '2', is_read: false, in_stock: false, comic: 3, volume: null} +- model: comics.issue + pk: 678 + fields: {number: '3', is_read: false, in_stock: false, comic: 3, volume: null} +- model: comics.issue + pk: 679 + fields: {number: '503', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 680 + fields: {number: '504', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 681 + fields: {number: '505', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 682 + fields: {number: '506', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 683 + fields: {number: '507', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 684 + fields: {number: '508', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 685 + fields: {number: '509', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 686 + fields: {number: '510', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 687 + fields: {number: '511', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 688 + fields: {number: '512', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 689 + fields: {number: '513', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 690 + fields: {number: '514', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 691 + fields: {number: '515', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 692 + fields: {number: '516', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 693 + fields: {number: '517', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 694 + fields: {number: '518', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 695 + fields: {number: '519', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 696 + fields: {number: '520', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 697 + fields: {number: '521', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 698 + fields: {number: '522', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 699 + fields: {number: '523', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 700 + fields: {number: '524', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 701 + fields: {number: '525', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 702 + fields: {number: '526', is_read: false, in_stock: false, comic: 5, volume: null} +- model: comics.issue + pk: 703 + fields: {number: '1', is_read: false, in_stock: false, comic: 26, volume: null} +- model: comics.issue + pk: 704 + fields: {number: '2', is_read: false, in_stock: false, comic: 26, volume: null} +- model: comics.issue + pk: 705 + fields: {number: '1', is_read: false, in_stock: false, comic: 28, volume: null} +- model: comics.issue + pk: 706 + fields: {number: '2', is_read: false, in_stock: false, comic: 28, volume: null} +- model: comics.issue + pk: 707 + fields: {number: '1', is_read: false, in_stock: false, comic: 29, volume: null} +- model: comics.issue + pk: 708 + fields: {number: '1', is_read: false, in_stock: false, comic: 30, volume: null} +- model: comics.issue + pk: 709 + fields: {number: '1', is_read: false, in_stock: false, comic: 31, volume: null} +- model: comics.issue + pk: 710 + fields: {number: '17', is_read: false, in_stock: false, comic: 32, volume: null} +- model: comics.issue + pk: 711 + fields: {number: '18', is_read: false, in_stock: false, comic: 32, volume: null} +- model: comics.issue + pk: 712 + fields: {number: '19', is_read: false, in_stock: false, comic: 32, volume: null} +- model: comics.issue + pk: 713 + fields: {number: '20', is_read: false, in_stock: false, comic: 32, volume: null} +- model: comics.issue + pk: 714 + fields: {number: '21', is_read: false, in_stock: false, comic: 32, volume: null} +- model: comics.issue + pk: 715 + fields: {number: '22', is_read: false, in_stock: false, comic: 32, volume: null} +- model: comics.issue + pk: 716 + fields: {number: '23', is_read: false, in_stock: false, comic: 32, volume: null} +- model: comics.issue + pk: 717 + fields: {number: '1', is_read: false, in_stock: false, comic: 7, volume: null} +- model: comics.issue + pk: 718 + fields: {number: '2', is_read: false, in_stock: false, comic: 7, volume: null} +- model: comics.issue + pk: 719 + fields: {number: '3', is_read: false, in_stock: false, comic: 7, volume: null} +- model: comics.issue + pk: 720 + fields: {number: '4', is_read: false, in_stock: false, comic: 7, volume: null} +- model: comics.issue + pk: 721 + fields: {number: '1', is_read: false, in_stock: false, comic: 155, volume: null} +- model: comics.issue + pk: 722 + fields: {number: '2', is_read: false, in_stock: false, comic: 155, volume: null} +- model: comics.issue + pk: 723 + fields: {number: '3', is_read: false, in_stock: false, comic: 155, volume: null} +- model: comics.issue + pk: 724 + fields: {number: '4', is_read: false, in_stock: false, comic: 155, volume: null} +- model: comics.issue + pk: 725 + fields: {number: '5', is_read: false, in_stock: false, comic: 155, volume: null} +- model: comics.issue + pk: 726 + fields: {number: '6', is_read: false, in_stock: false, comic: 155, volume: null} +- model: comics.issue + pk: 727 + fields: {number: '1', is_read: false, in_stock: false, comic: 156, volume: null} +- model: comics.issue + pk: 728 + fields: {number: '2', is_read: false, in_stock: false, comic: 156, volume: null} +- model: comics.issue + pk: 729 + fields: {number: '1', is_read: false, in_stock: false, comic: 157, volume: null} +- model: comics.issue + pk: 730 + fields: {number: '2', is_read: false, in_stock: false, comic: 157, volume: null} +- model: comics.issue + pk: 731 + fields: {number: '3', is_read: false, in_stock: false, comic: 157, volume: null} +- model: comics.issue + pk: 732 + fields: {number: '4', is_read: false, in_stock: false, comic: 157, volume: null} +- model: comics.issue + pk: 733 + fields: {number: '1', is_read: false, in_stock: false, comic: 158, volume: null} +- model: comics.issue + pk: 734 + fields: {number: '2', is_read: false, in_stock: false, comic: 158, volume: null} +- model: comics.issue + pk: 735 + fields: {number: '3', is_read: false, in_stock: false, comic: 158, volume: null} +- model: comics.issue + pk: 736 + fields: {number: '4', is_read: false, in_stock: false, comic: 158, volume: null} +- model: comics.issue + pk: 737 + fields: {number: '1', is_read: false, in_stock: false, comic: 159, volume: null} +- model: comics.issue + pk: 738 + fields: {number: '2', is_read: false, in_stock: false, comic: 159, volume: null} +- model: comics.issue + pk: 739 + fields: {number: '1', is_read: false, in_stock: false, comic: 58, volume: null} +- model: comics.issue + pk: 740 + fields: {number: '2', is_read: false, in_stock: false, comic: 58, volume: null} +- model: comics.issue + pk: 741 + fields: {number: '1', is_read: false, in_stock: false, comic: 60, volume: null} +- model: comics.storyarc + pk: 1 + fields: {name: Higher Learning, comic: 39} +- model: comics.storyarc + pk: 2 + fields: {name: Mind Games, comic: 39} +- model: comics.storyarc + pk: 3 + fields: {name: Bloom, comic: 39} +- model: comics.tradepaperback + pk: 1 + fields: {name: Vol. 1, comic: 79, issue_start: 1, issue_end: 12} +- model: comics.tradepaperback + pk: 2 + fields: {name: From The Ashes, comic: 106, issue_start: 1, issue_end: 6} +- model: comics.tradepaperback + pk: 3 + fields: {name: The Dragons Tale, comic: 106, issue_start: 7, issue_end: 12} +- model: comics.tradepaperback + pk: 4 + fields: {name: The Warriors Tale, comic: 106, issue_start: 13, issue_end: 18} +- model: comics.tradepaperback + pk: 5 + fields: {name: The Thiefs Tale, comic: 106, issue_start: 19, issue_end: 24} +- model: comics.tradepaperback + pk: 6 + fields: {name: Vol. 1, comic: 153, issue_start: 1, issue_end: 18} +- model: comics.tradepaperback + pk: 7 + fields: {name: Choices, comic: 57, issue_start: 1, issue_end: 5} +- model: comics.tradepaperback + pk: 8 + fields: {name: Atlantis Rising, comic: 154, issue_start: 1, issue_end: 6} +- model: comics.tradepaperback + pk: 9 + fields: {name: Test Of Time, comic: 154, issue_start: 7, issue_end: 12} +- model: comics.tradepaperback + pk: 10 + fields: {name: Strangers in Atlantis, comic: 154, issue_start: 13, issue_end: 18} +- model: comics.tradepaperback + pk: 11 + fields: {name: Flying Solo, comic: 78, issue_start: 1, issue_end: 7} +- model: comics.tradepaperback + pk: 12 + fields: {name: Going To Ground, comic: 78, issue_start: 8, issue_end: 14} +- model: comics.tradepaperback + pk: 13 + fields: {name: Taking The Skies, comic: 78, issue_start: 15, issue_end: 20} +- model: comics.tradepaperback + pk: 14 + fields: {name: Coming Home, comic: 78, issue_start: 21, issue_end: 26} +- model: comics.tradepaperback + pk: 15 + fields: {name: Rite of Passage, comic: 82, issue_start: 1, issue_end: 7} +- model: comics.tradepaperback + pk: 16 + fields: {name: The Demon Queen, comic: 82, issue_start: 8, issue_end: 14} +- model: comics.tradepaperback + pk: 17 + fields: {name: Siege of Scales, comic: 82, issue_start: 15, issue_end: 20} +- model: comics.tradepaperback + pk: 18 + fields: {name: Out All Night, comic: 82, issue_start: 21, issue_end: 26} +- model: comics.tradepaperback + pk: 19 + fields: {name: Single Green Female, comic: 101, issue_start: 1, issue_end: 6} +- model: comics.tradepaperback + pk: 20 + fields: {name: Superhuman Law, comic: 101, issue_start: 7, issue_end: 12} +- model: comics.tradepaperback + pk: 21 + fields: {name: Conflict of Conscience, comic: 99, issue_start: 1, issue_end: 7} +- model: comics.tradepaperback + pk: 22 + fields: {name: Blood For Blood, comic: 99, issue_start: 8, issue_end: 14} +- model: comics.tradepaperback + pk: 23 + fields: {name: Divided Loyalties, comic: 99, issue_start: 15, issue_end: 21} +- model: comics.tradepaperback + pk: 24 + fields: {name: Sanctuary, comic: 99, issue_start: 22, issue_end: 27} +- model: comics.tradepaperback + pk: 25 + fields: {name: The End Of History, comic: 136, issue_start: 444, issue_end: 449} +- model: comics.tradepaperback + pk: 26 + fields: {name: Public Enemies, comic: 122, issue_start: 1, issue_end: 6} +- model: comics.tradepaperback + pk: 27 + fields: {name: Loyalty And Loss, comic: 23, issue_start: 1, issue_end: 6} +- model: comics.tradepaperback + pk: 28 + fields: {name: Heaven & Earth, comic: 23, issue_start: 7, issue_end: 12} +- model: comics.tradepaperback + pk: 29 + fields: {name: Earth Angel, comic: 23, issue_start: 13, issue_end: 18} +- model: comics.tradepaperback + pk: 30 + fields: {name: Redemption, comic: 23, issue_start: 19, issue_end: 24} +- model: comics.tradepaperback + pk: 31 + fields: {name: '1602', comic: 1, issue_start: 1, issue_end: 8} +- model: comics.tradepaperback + pk: 32 + fields: {name: Coming Home, comic: 5, issue_start: 30, issue_end: 35} +- model: comics.tradepaperback + pk: 33 + fields: {name: Revelations, comic: 5, issue_start: 36, issue_end: 39} +- model: comics.tradepaperback + pk: 34 + fields: {name: Until the Stars Turn Cold, comic: 5, issue_start: 40, issue_end: 45} +- model: comics.tradepaperback + pk: 35 + fields: {name: The Life & Death of Spiders, comic: 5, issue_start: 46, issue_end: 50} +- model: comics.tradepaperback + pk: 36 + fields: {name: Unintended Consequences, comic: 5, issue_start: 51, issue_end: 56} +- model: comics.tradepaperback + pk: 37 + fields: {name: Happy Birthday, comic: 5, issue_start: 500, issue_end: 502} +- model: comics.tradepaperback + pk: 38 + fields: {name: Sonderband 1, comic: 25, issue_start: 1, issue_end: 2} +- model: comics.tradepaperback + pk: 39 + fields: {name: Of Like Minds, comic: 17, issue_start: 56, issue_end: 61} +- model: comics.tradepaperback + pk: 40 + fields: {name: Sensei & Student, comic: 17, issue_start: 62, issue_end: 68} diff --git a/django/kontor/comics/migrations/0001_initial.py b/django/kontor/comics/migrations/0001_initial.py new file mode 100644 index 0000000..fa40cd2 --- /dev/null +++ b/django/kontor/comics/migrations/0001_initial.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +<<<<<<< HEAD +# Generated by Django 1.9.6 on 2016-06-13 15:33 +======= +# Generated by Django 1.9.5 on 2016-04-10 20:23 +>>>>>>> ignore compiled scripts +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Artist', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='Comic', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=40)), + ('current_order', models.BooleanField()), + ('completed', models.BooleanField()), +<<<<<<< HEAD + ('artist', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='artist', to='comics.Artist')), +======= +>>>>>>> ignore compiled scripts + ], + ), + migrations.CreateModel( + name='Issue', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('number', models.CharField(max_length=4)), + ('is_read', models.BooleanField(default=False)), + ('in_stock', models.BooleanField(default=False)), + ('comic', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='comics.Comic')), + ], + ), + migrations.CreateModel( + name='Publisher', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='StoryArc', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('comic', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='comics.Comic')), + ], + ), + migrations.CreateModel( + name='TradePaperback', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ('issue_start', models.IntegerField()), + ('issue_end', models.IntegerField()), + ('comic', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='comics.Comic')), + ], + ), + migrations.CreateModel( + name='Volume', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ('comic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='comics.Comic')), + ], + ), + migrations.AddField( +<<<<<<< HEAD + model_name='issue', + name='volume', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='comics.Volume'), + ), + migrations.AddField( +======= +>>>>>>> ignore compiled scripts + model_name='comic', + name='publisher', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='comics.Publisher'), + ), +<<<<<<< HEAD + migrations.AddField( + model_name='comic', + name='writer', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='writer', to='comics.Artist'), + ), +======= +>>>>>>> ignore compiled scripts + ] diff --git a/django/kontor/comics/migrations/0002_auto_20160620_1405.py b/django/kontor/comics/migrations/0002_auto_20160620_1405.py new file mode 100644 index 0000000..8bec639 --- /dev/null +++ b/django/kontor/comics/migrations/0002_auto_20160620_1405.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-20 14:05 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('comics', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='comic', + name='artist', + field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='artist', to='comics.Artist'), + ), + migrations.AlterField( + model_name='comic', + name='writer', + field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='writer', to='comics.Artist'), + ), + ] diff --git a/django/kontor/comics/migrations/0003_auto_20160620_1410.py b/django/kontor/comics/migrations/0003_auto_20160620_1410.py new file mode 100644 index 0000000..bf0d181 --- /dev/null +++ b/django/kontor/comics/migrations/0003_auto_20160620_1410.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-20 14:10 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('comics', '0002_auto_20160620_1405'), + ] + + operations = [ + migrations.AlterField( + model_name='comic', + name='artist', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='artist', to='comics.Artist'), + ), + migrations.AlterField( + model_name='comic', + name='writer', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='writer', to='comics.Artist'), + ), + ] diff --git a/django/kontor/comics/migrations/0004_auto_20160622_1122.py b/django/kontor/comics/migrations/0004_auto_20160622_1122.py new file mode 100644 index 0000000..f9d7312 --- /dev/null +++ b/django/kontor/comics/migrations/0004_auto_20160622_1122.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-22 11:22 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('comics', '0003_auto_20160620_1410'), + ] + + operations = [ + migrations.AlterField( + model_name='issue', + name='volume', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comics.Volume'), + ), + ] diff --git a/django/kontor/comics/migrations/0005_auto_20160622_1146.py b/django/kontor/comics/migrations/0005_auto_20160622_1146.py new file mode 100644 index 0000000..d7cae42 --- /dev/null +++ b/django/kontor/comics/migrations/0005_auto_20160622_1146.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-22 11:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('comics', '0004_auto_20160622_1122'), + ] + + operations = [ + migrations.AlterField( + model_name='comic', + name='title', + field=models.CharField(max_length=60), + ), + ] diff --git a/django/kontor/comics/migrations/__init__.py b/django/kontor/comics/migrations/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/django/kontor/comics/models.py b/django/kontor/comics/models.py new file mode 100755 index 0000000..5e55d5f --- /dev/null +++ b/django/kontor/comics/models.py @@ -0,0 +1,101 @@ +from __future__ import unicode_literals + +from django.db import models + +# Create your models here. +<<<<<<< HEAD +<<<<<<< HEAD + +======= +>>>>>>> add Comic models + + +class Publisher(models.Model): + name = models.CharField(max_length=100) + + def __str__(self): + return self.name + + +class Artist(models.Model): + name = models.CharField(max_length=100) + + def __str__(self): + return self.name + + +class Comic(models.Model): +<<<<<<< HEAD + title = models.CharField(max_length=60) + publisher = models.ForeignKey(Publisher) + current_order = models.BooleanField() + completed = models.BooleanField() + writer = models.ForeignKey(Artist, blank=True, null=True, related_name='writer') + artist = models.ForeignKey(Artist, blank=True, null=True, related_name='artist') +======= + title = models.CharField(max_length=40) + publisher = models.ForeignKey(Publisher) + current_order = models.BooleanField() + completed = models.BooleanField() +<<<<<<< HEAD +>>>>>>> add Comic models +======= + writer = models.ForeignKey(Artist, null=True, related_name='writer') + artist = models.ForeignKey(Artist, null=True, related_name='artist') +>>>>>>> backup + + def __str__(self): + return self.title + + +class Volume(models.Model): + name = models.CharField(max_length=40) + comic = models.ForeignKey(Comic) + + def __str__(self): + return self.comic.title + ' ' + self.name + + +class Issue(models.Model): + number = models.CharField(max_length=4) + is_read = models.BooleanField(default=False) + in_stock = models.BooleanField(default=False) + comic = models.ForeignKey(Comic, null=True) +<<<<<<< HEAD +<<<<<<< HEAD + volume = models.ForeignKey(Volume, blank=True, null=True) +======= +>>>>>>> add Comic models +======= + volume = models.ForeignKey(Volume, null=True) +>>>>>>> backup + + def __str__(self): + return self.comic.title + ' #' + self.number + + +class StoryArc(models.Model): + name = models.CharField(max_length=100) + comic = models.ForeignKey(Comic, null=True) + + def __str__(self): + return self.name + + +class TradePaperback(models.Model): + name = models.CharField(max_length=40) + comic = models.ForeignKey(Comic, null=True) + issue_start = models.IntegerField() + issue_end = models.IntegerField() + + def __str__(self): +<<<<<<< HEAD + return self.comic.title + ' TP ' + self.name +<<<<<<< HEAD +======= +>>>>>>> initial setup +======= +>>>>>>> add Comic models +======= + return self.comic.title + ' TP ' + self.name +>>>>>>> backup diff --git a/django/kontor/comics/static/comics/style.css b/django/kontor/comics/static/comics/style.css new file mode 100644 index 0000000..bb3a4cd --- /dev/null +++ b/django/kontor/comics/static/comics/style.css @@ -0,0 +1,6 @@ +li a { + color: green; +} +td a { + color: green; +} diff --git a/django/kontor/comics/templates/comics/detail.html b/django/kontor/comics/templates/comics/detail.html new file mode 100755 index 0000000..dc11ccd --- /dev/null +++ b/django/kontor/comics/templates/comics/detail.html @@ -0,0 +1,28 @@ +<<<<<<< HEAD +{% extends "kontor/base.html" %} +{% block title %}Comic Details{% endblock %} +{% block content %} +======= + + + + + Comic Details + + +>>>>>>> backup + + + + + + + +
Namecurrent Ordercompleted
{{ comic.title }}{{ comic.current_order }}{{ comic.completed }}
+<<<<<<< HEAD +{% endblock %} +======= + + + +>>>>>>> backup diff --git a/django/kontor/comics/templates/comics/index.html b/django/kontor/comics/templates/comics/index.html new file mode 100755 index 0000000..4a821e8 --- /dev/null +++ b/django/kontor/comics/templates/comics/index.html @@ -0,0 +1,34 @@ +<<<<<<< HEAD +{% extends "kontor/base.html" %} +{% block title %}Comic Overview{% endblock %} +{% block content %} +======= +{% load staticfiles %} + + + + + + + + + Comic Overview + + +>>>>>>> backup + + + {% for comic in comic_list %} + + + + + + {% endfor %} +
Namecurrent Ordercompleted
{{ comic.title }}{{ comic.current_order }}{{ comic.completed }}
+<<<<<<< HEAD +{% endblock %} +======= + + +>>>>>>> backup diff --git a/django/kontor/comics/tests.py b/django/kontor/comics/tests.py new file mode 100644 index 0000000..3187d6f --- /dev/null +++ b/django/kontor/comics/tests.py @@ -0,0 +1,8 @@ +from django.test import TestCase + +# Create your tests here. + +class ComicsViewsTestCase(TestCase): + def test_index(self): + resp = self.client.get('/comics/') + self.assertEqual(resp.status_code, 200) diff --git a/django/kontor/comics/urls.py b/django/kontor/comics/urls.py new file mode 100644 index 0000000..c16537f --- /dev/null +++ b/django/kontor/comics/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url + +from . import views + +app_name = 'comics' +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^(?P[0-9]+)/$', views.DetailView.as_view(), name='detail'), +] + diff --git a/django/kontor/comics/views.py b/django/kontor/comics/views.py new file mode 100644 index 0000000..5bb5086 --- /dev/null +++ b/django/kontor/comics/views.py @@ -0,0 +1,51 @@ +<<<<<<< HEAD +from django.shortcuts import render +<<<<<<< HEAD +from django.http import HttpResponseRedirect +from django.core.urlresolvers import reverse +from django.views import generic + +from .models import Comic +# Create your views here. + + +class IndexView(generic.ListView): + template_name = 'comics/index.html' + context_object_name = 'comic_list' + + def get_queryset(self): + """Return the list of comics.""" + return Comic.objects.all() + + +class DetailView(generic.DetailView): + model = Comic + template_name = 'comics/detail.html' +======= +======= +from django.shortcuts import get_object_or_404, render +from django.http import HttpResponseRedirect +from django.core.urlresolvers import reverse +from django.views import generic +>>>>>>> backup + +from .models import Comic +# Create your views here. +<<<<<<< HEAD +>>>>>>> initial setup +======= + + +class IndexView(generic.ListView): + template_name = 'comics/index.html' + context_object_name = 'comic_list' + + def get_queryset(self): + """Return the list of comics.""" + return Comic.objects.all() + + +class DetailView(generic.DetailView): + model = Comic + template_name = 'comics/detail.html' +>>>>>>> backup diff --git a/django/kontor/homeoffice/__init__.py b/django/kontor/homeoffice/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/django/kontor/homeoffice/admin.py b/django/kontor/homeoffice/admin.py new file mode 100755 index 0000000..8c38f3f --- /dev/null +++ b/django/kontor/homeoffice/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/django/kontor/homeoffice/apps.py b/django/kontor/homeoffice/apps.py new file mode 100755 index 0000000..d7e7e57 --- /dev/null +++ b/django/kontor/homeoffice/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class HomeOfficeConfig(AppConfig): + name = 'homeoffice' diff --git a/django/kontor/homeoffice/migrations/0001_initial.py b/django/kontor/homeoffice/migrations/0001_initial.py new file mode 100644 index 0000000..073e71b --- /dev/null +++ b/django/kontor/homeoffice/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-20 16:58 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Expense', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='Travel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + ] diff --git a/django/kontor/homeoffice/migrations/__init__.py b/django/kontor/homeoffice/migrations/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/django/kontor/homeoffice/models.py b/django/kontor/homeoffice/models.py new file mode 100755 index 0000000..ebe8ae9 --- /dev/null +++ b/django/kontor/homeoffice/models.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals + +from django.db import models + +# Create your models here. +class Travel(models.Model): + pass + +class Expense(models.Model): + pass diff --git a/django/kontor/homeoffice/templates/homeoffice/index.html b/django/kontor/homeoffice/templates/homeoffice/index.html new file mode 100755 index 0000000..5c9a259 --- /dev/null +++ b/django/kontor/homeoffice/templates/homeoffice/index.html @@ -0,0 +1,2 @@ +{% extends "kontor/base.html" %} +{% block title %}HomeOffice{% endblock %} diff --git a/django/kontor/homeoffice/tests.py b/django/kontor/homeoffice/tests.py new file mode 100755 index 0000000..7ce503c --- /dev/null +++ b/django/kontor/homeoffice/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/django/kontor/homeoffice/urls.py b/django/kontor/homeoffice/urls.py new file mode 100755 index 0000000..0c7cd34 --- /dev/null +++ b/django/kontor/homeoffice/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import url + +from . import views + +app_name = 'homeoffice' +urlpatterns = [ + url(r'^$', views.index, name='index'), +] diff --git a/django/kontor/homeoffice/views.py b/django/kontor/homeoffice/views.py new file mode 100755 index 0000000..a0b0185 --- /dev/null +++ b/django/kontor/homeoffice/views.py @@ -0,0 +1,5 @@ +from django.shortcuts import render + +# Create your views here. +def index(request): + return render(request, 'homeoffice/index.html') diff --git a/django/kontor/kontor/__init__.py b/django/kontor/kontor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/kontor/kontor/settings.py b/django/kontor/kontor/settings.py new file mode 100755 index 0000000..0bcce73 --- /dev/null +++ b/django/kontor/kontor/settings.py @@ -0,0 +1,166 @@ +""" +Django settings for kontor project. + +<<<<<<< HEAD +<<<<<<< HEAD +Generated by 'django-admin startproject' using Django 1.9.6. +======= +Generated by 'django-admin startproject' using Django 1.9.5. +>>>>>>> initial setup + +======= +>>>>>>> backup +For more information on this file, see +https://docs.djangoproject.com/en/1.6/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.6/ref/settings/ +""" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +<<<<<<< HEAD +<<<<<<< HEAD +SECRET_KEY = '8s-(!r%0yu)b(hdkes_v#g8kp7)0ma851j+&43cds#duk!s-_c' +======= +SECRET_KEY = 'c)&86w07k)091bgi1llt+aol5$8in8g=n#+iba4784cdw$#h)^' +>>>>>>> initial setup +======= +SECRET_KEY = 'i5qx(%mex)4ovh#y4m94b1(3xiw4%8+rx-!kpw9v4q*@0v6pd2' +>>>>>>> backup + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +TEMPLATE_DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +<<<<<<< HEAD +INSTALLED_APPS = [ +<<<<<<< HEAD +<<<<<<< HEAD + 'comics.apps.ComicsConfig', + 'library.apps.LibraryConfig', + 'tysc.apps.TyscConfig', + 'tradingcards.apps.TradingCardsConfig', + 'homeoffice.apps.HomeOfficeConfig', + 'medien.apps.MedienConfig', +======= +>>>>>>> initial setup +======= + 'comics.apps.ComicsConfig', +>>>>>>> add admin site for Comics +======= +INSTALLED_APPS = ( + 'comics.apps.ComicsConfig', + 'tysc.apps.TyscConfig', + 'library.apps.LibraryConfig', +>>>>>>> backup + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +) + +MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = 'kontor.urls' + +<<<<<<< HEAD +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', +<<<<<<< HEAD + 'DIRS': [os.path.join(BASE_DIR, 'templates')], +======= + 'DIRS': [], +>>>>>>> initial setup + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', +<<<<<<< HEAD + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.static', + 'django.template.context_processors.tz', +======= +>>>>>>> initial setup + ], + }, + }, +] + +======= +>>>>>>> backup +WSGI_APPLICATION = 'kontor.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.6/ref/settings/#databases + +DATABASES = { + 'default': { +# 'ENGINE': 'django.db.backends.dummy', + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + +#from mongoengine import connect +#connect('kontor') + +#SESSION_ENGINE = 'mongoengine.django.sessions' +#SESSION_SERIALIZER = 'mongoengine.django.sessions.BSONSerializer' + +# Internationalization +# https://docs.djangoproject.com/en/1.6/topics/i18n/ + +<<<<<<< HEAD +LANGUAGE_CODE = 'de-de' +======= +LANGUAGE_CODE = 'en-us' +>>>>>>> initial setup + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.6/howto/static-files/ + +STATIC_URL = '/static/' +<<<<<<< HEAD + +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, "static"), +] +======= +>>>>>>> initial setup diff --git a/django/kontor/kontor/urls.py b/django/kontor/kontor/urls.py new file mode 100755 index 0000000..043a7e5 --- /dev/null +++ b/django/kontor/kontor/urls.py @@ -0,0 +1,61 @@ +from django.conf.urls import patterns, include, url + +<<<<<<< HEAD +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.9/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +<<<<<<< HEAD +from django.conf.urls import url, include +from django.contrib import admin +from django.contrib.auth import views as auth_views + +from . import views + +admin.autodiscover() + +urlpatterns = [ + #url(r'^admin/', admin.site.urls), + url('^', include('django.contrib.auth.urls')), + url(r'^$', views.index, name='index'), + #url(r'^accounts/login/$', auth_views.login), + url(r'^accounts/login/$', auth_views.login, {'template_name': 'kontor/login.html'}), + url(r'^accounts/logout/$', auth_views.logout ), + url(r'^admin/', include(admin.site.urls)), + url(r'^comics/', include('comics.urls')), + url(r'^office/', include('homeoffice.urls')), + url(r'^tradingcards/', include('tradingcards.urls')), + url(r'^tysc/', include('tysc.urls')), + url(r'^library/', include('library.urls')), + url(r'^medien/', include('medien.urls')), +======= +from django.conf.urls import url +======= +>>>>>>> backup +from django.contrib import admin +admin.autodiscover() + +<<<<<<< HEAD +urlpatterns = [ + url(r'^admin/', admin.site.urls), +>>>>>>> initial setup +] +======= +urlpatterns = patterns('', + # Examples: + #url(r'^$', 'kontor.views.home', name='home'), + # url(r'^blog/', include('blog.urls')), + url(r'^comics/', include('comics.urls')), + url(r'^tysc/', include('tysc.urls')), + url(r'^admin/', include(admin.site.urls)), +) +>>>>>>> backup diff --git a/django/kontor/kontor/views.py b/django/kontor/kontor/views.py new file mode 100755 index 0000000..357acb3 --- /dev/null +++ b/django/kontor/kontor/views.py @@ -0,0 +1,14 @@ +from django.shortcuts import render +from django.http import HttpResponse + + +def index(request): +<<<<<<< HEAD + return render(request, 'kontor/index.html') +======= + return HttpResponse('You are in the Kontor application') +>>>>>>> backup + + +def home(request): + return HttpResponse('You are in the Kontor application') diff --git a/django/kontor/kontor/wsgi.py b/django/kontor/kontor/wsgi.py new file mode 100644 index 0000000..3e50929 --- /dev/null +++ b/django/kontor/kontor/wsgi.py @@ -0,0 +1,14 @@ +""" +WSGI config for kontor project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ +""" + +import os +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "kontor.settings") + +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() diff --git a/django/kontor/library/__init__.py b/django/kontor/library/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/kontor/library/admin.py b/django/kontor/library/admin.py new file mode 100644 index 0000000..2359803 --- /dev/null +++ b/django/kontor/library/admin.py @@ -0,0 +1,20 @@ +from django.contrib import admin + +from .models import Author +from .models import Book +<<<<<<< HEAD +from .models import Category +from .models import Edition +from .models import Publisher +======= +>>>>>>> backup + +# Register your models here. +admin.site.register(Author) +admin.site.register(Book) +<<<<<<< HEAD +admin.site.register(Category) +admin.site.register(Edition) +admin.site.register(Publisher) +======= +>>>>>>> backup diff --git a/django/kontor/library/apps.py b/django/kontor/library/apps.py new file mode 100644 index 0000000..b85578c --- /dev/null +++ b/django/kontor/library/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class LibraryConfig(AppConfig): + name = 'library' diff --git a/django/kontor/library/fixtures/books.yaml b/django/kontor/library/fixtures/books.yaml new file mode 100644 index 0000000..84d3f1c --- /dev/null +++ b/django/kontor/library/fixtures/books.yaml @@ -0,0 +1,473 @@ +- model: library.author + pk: 1 + fields: {name: Terry Pratchett} +- model: library.author + pk: 2 + fields: {name: Colin Forbes} +- model: library.author + pk: 3 + fields: {name: Markus Heitz} +- model: library.author + pk: 4 + fields: {name: Ralf Kuehnel} +- model: library.author + pk: 5 + fields: {name: Richard Murch} +- model: library.author + pk: 6 + fields: {name: Gunter Saake} +- model: library.author + pk: 7 + fields: {name: Kai-Uwe Sattler} +- model: library.author + pk: 8 + fields: {name: Joerg Thadeusz} +- model: library.author + pk: 9 + fields: {name: Stephan Burgdorff} +- model: library.author + pk: 10 + fields: {name: Christian Habbe} +- model: library.author + pk: 11 + fields: {name: Stephen Kenson} +- model: library.author + pk: 12 + fields: {name: Martin Fowler} +- model: library.author + pk: 13 + fields: {name: Ramnivas Laddad} +- model: library.author + pk: 14 + fields: {name: Thomas Gifford} +- model: library.author + pk: 15 + fields: {name: Rene Goscinny} +- model: library.author + pk: 16 + fields: {name: Albert Uderzo} +- model: library.author + pk: 17 + fields: {name: Mel Odem} +- model: library.author + pk: 18 + fields: {name: Harri Assmann} +- model: library.author + pk: 19 + fields: {name: Harald Schmidt} +- model: library.author + pk: 20 + fields: {name: Bruce Tate} +- model: library.author + pk: 21 + fields: {name: Mike Clark} +- model: library.author + pk: 22 + fields: {name: Bob Lee} +- model: library.author + pk: 23 + fields: {name: Patrick Linskey} +- model: library.author + pk: 24 + fields: {name: Jim Knipfel} +- model: library.author + pk: 25 + fields: {name: Carl Sargent} +- model: library.author + pk: 26 + fields: {name: Beate Sauer} +- model: library.author + pk: 27 + fields: {name: Kent Beck} +- model: library.author + pk: 28 + fields: {name: Erich Gamma} +- model: library.author + pk: 29 + fields: {name: Ilko-Sascha Kowalczuk} +- model: library.author + pk: 30 + fields: {name: Dalai Lama} +- model: library.author + pk: 31 + fields: {name: Horst Saecker} +- model: library.author + pk: 32 + fields: {name: Romain Sardou} +- model: library.author + pk: 33 + fields: {name: Berthold Daum} +- model: library.author + pk: 34 + fields: {name: Javier Sierra} +- model: library.author + pk: 35 + fields: {name: Dieter Hesselberger} +- model: library.author + pk: 36 + fields: {name: Anette Baecker} +- model: library.author + pk: 37 + fields: {name: Paul Baecker} +- model: library.author + pk: 38 + fields: {name: Lisa Smedmann} +- model: library.author + pk: 39 + fields: {name: Michael Cordy} +- model: library.author + pk: 40 + fields: {name: Stephen Fry} +- model: library.author + pk: 41 + fields: {name: Jonathan Bond} +- model: library.author + pk: 42 + fields: {name: Vince Flynn} +- model: library.author + pk: 43 + fields: {name: Eric-Emmanuel Schmitt} +- model: library.author + pk: 44 + fields: {name: Jak Koke} +- model: library.author + pk: 45 + fields: {name: Nigel Findley} +- model: library.author + pk: 46 + fields: {name: Edgar Noske} +- model: library.author + pk: 47 + fields: {name: Hannes Wertheim} +- model: library.author + pk: 48 + fields: {name: Frank Schaetzing} +- model: library.author + pk: 49 + fields: {name: Frank Pilz} +- model: library.author + pk: 50 + fields: {name: Frederick Forsyth} +- model: library.author + pk: 51 + fields: {name: Chris Kubasik} +- model: library.author + pk: 52 + fields: {name: Jordan K. Weisman} +- model: library.author + pk: 53 + fields: {name: Hans Georg Lehmann} +- model: library.author + pk: 54 + fields: {name: Nyx Smith} +- model: library.author + pk: 55 + fields: {name: Ferdinand Seibt} +- model: library.author + pk: 56 + fields: {name: Christopher Moore} +- model: library.author + pk: 57 + fields: {name: Jens Gieseke} +- model: library.author + pk: 58 + fields: {name: Bernd Lindner} +- model: library.author + pk: 59 + fields: {name: Horst Poetzsch} +- model: library.author + pk: 60 + fields: {name: Caroline Spector} +- model: library.author + pk: 61 + fields: {name: Peter Godman} +- model: library.author + pk: 62 + fields: {name: Hans Joachim Alpers} +- model: library.author + pk: 63 + fields: {name: Bernhard Schlink} +- model: library.author + pk: 64 + fields: {name: Rebecca Gable} +- model: library.author + pk: 65 + fields: {name: Douglas Adams} +- model: library.author + pk: 66 + fields: {name: Susan Cooper} +- model: library.author + pk: 67 + fields: {name: Donna W. Cross} +- model: library.author + pk: 68 + fields: {name: Peter Prange} +- model: library.author + pk: 69 + fields: {name: Tanja Kinkel} +- model: library.author + pk: 70 + fields: {name: Ken Follett} +- model: library.author + pk: 71 + fields: {name: Minette Walters} +- model: library.author + pk: 72 + fields: {name: Peter Tremayne} +- model: library.author + pk: 73 + fields: {name: Marion Zimmer Bradley} +- model: library.author + pk: 74 + fields: {name: Wolfgang Hohlbein} +- model: library.author + pk: 75 + fields: {name: Friedrich Wilhelm Graf} +- model: library.author + pk: 76 + fields: {name: Nyx Smith} +- model: library.author + pk: 78 + fields: {name: Ferdinand Seibt} +- model: library.author + pk: 79 + fields: {name: Christopher Moore} +- model: library.author + pk: 80 + fields: {name: Jens Gieseke} +- model: library.author + pk: 81 + fields: {name: Bernd Lindner} +- model: library.author + pk: 82 + fields: {name: Horst Poetzsch} +- model: library.author + pk: 84 + fields: {name: Caroline Spector} +- model: library.author + pk: 85 + fields: {name: Peter Godman} +- model: library.author + pk: 86 + fields: {name: Hans Joachim Alpers} +- model: library.author + pk: 87 + fields: {name: Bernhard Schlink} +- model: library.author + pk: 89 + fields: {name: Rebecca Gable} +- model: library.author + pk: 90 + fields: {name: Douglas Adams} +- model: library.author + pk: 91 + fields: {name: Susan Cooper} +- model: library.author + pk: 93 + fields: {name: Donna W. Cross} +- model: library.author + pk: 94 + fields: {name: Peter Prange} +- model: library.author + pk: 95 + fields: {name: Tanja Kinkel} +- model: library.author + pk: 96 + fields: {name: Ken Follett} +- model: library.author + pk: 97 + fields: {name: Minette Walters} +- model: library.author + pk: 99 + fields: {name: Peter Tremayne} +- model: library.author + pk: 101 + fields: {name: Marion Zimmer Bradley} +- model: library.author + pk: 102 + fields: {name: Wolfgang Hohlbein} +- model: library.author + pk: 103 + fields: {name: Friedrich Wilhelm Graf} +- model: library.author + pk: 104 + fields: {name: Terry Pratchett} +- model: library.author + pk: 105 + fields: {name: Daniel Suarez} +- model: library.category + pk: 1 + fields: {name: Science Fiction} +- model: library.category + pk: 2 + fields: {name: Roman} +- model: library.category + pk: 3 + fields: {name: Reference} +- model: library.category + pk: 4 + fields: {name: Fantasy} +- model: library.category + pk: 5 + fields: {name: Comics} +- model: library.category + pk: 6 + fields: {name: Satire} +- model: library.category + pk: 7 + fields: {name: Thriller} +- model: library.edition + pk: 1 + fields: {name: Paperback} +- model: library.edition + pk: 2 + fields: {name: Hardcover} +- model: library.edition + pk: 3 + fields: {name: Softcover} +- model: library.publisher + pk: 1 + fields: {name: Addison-Wesley Verlag} +- model: library.publisher + pk: 2 + fields: {name: O'Reilly Media} +- model: library.publisher + pk: 3 + fields: {name: O'Reilly & Associates} +- model: library.publisher + pk: 4 + fields: {name: Klett-Cotta} +- model: library.publisher + pk: 5 + fields: {name: Diogenes} +- model: library.publisher + pk: 6 + fields: {name: Jaron Verlag} +- model: library.publisher + pk: 7 + fields: {name: Merkur Verlag Rinteln} +- model: library.publisher + pk: 8 + fields: {name: Ehapa Verlag} +- model: library.publisher + pk: 9 + fields: {name: Markt+Technik Verlag} +- model: library.publisher + pk: 10 + fields: {name: Kiepenheuer & Witsch} +- model: library.publisher + pk: 11 + fields: {name: Goldmann Verlag} +- model: library.publisher + pk: 12 + fields: {name: Sams Publishing} +- model: library.publisher + pk: 13 + fields: {name: Wilhelm Heyne Verlag} +- model: library.publisher + pk: 14 + fields: {name: Amman Verlag & Co.} +- model: library.publisher + pk: 15 + fields: {name: Fischer Verlag} +- model: library.publisher + pk: 16 + fields: {name: BestBook} +- model: library.publisher + pk: 17 + fields: {name: List Verlag} +- model: library.publisher + pk: 18 + fields: {name: dpunkt.verlag GmbH} +- model: library.publisher + pk: 19 + fields: {name: Ullstein Verlag} +- model: library.publisher + pk: 20 + fields: {name: Piper Verlag} +- model: library.publisher + pk: 21 + fields: {name: Knaur Verlag} +- model: library.publisher + pk: 22 + fields: {name: Heyne Fantasy} +- model: library.publisher + pk: 23 + fields: {name: Manning Publications Co.} +- model: library.publisher + pk: 24 + fields: {name: ADAC Verlag} +- model: library.publisher + pk: 25 + fields: {name: dpunkt-Verlag} +- model: library.publisher + pk: 26 + fields: {name: Verlag Kiepenheuer & Witsch} +- model: library.publisher + pk: 27 + fields: {name: Hanser Wirtschaft} +- model: library.publisher + pk: 28 + fields: {name: Droemersche Verlagsanstalt} +- model: library.publisher + pk: 29 + fields: {name: Fischer Buecherei GmbH} +- model: library.publisher + pk: 30 + fields: {name: rororo Verlag} +- model: library.publisher + pk: 31 + fields: {name: Hoffmann und Campe} +- model: library.publisher + pk: 32 + fields: {name: Heyne Verlag} +- model: library.publisher + pk: 33 + fields: {name: dpunkt.verlag} +- model: library.publisher + pk: 34 + fields: {name: Dorling Kindersley} +- model: library.publisher + pk: 35 + fields: {name: Ammann Verlag} +- model: library.publisher + pk: 36 + fields: {name: Addison-Wesley} +- model: library.publisher + pk: 37 + fields: {name: Ueberreuter} +- model: library.publisher + pk: 38 + fields: {name: Bertelsmann Club} +- model: library.publisher + pk: 39 + fields: {name: Harriet Eder Verlag} +- model: library.publisher + pk: 40 + fields: {name: Marix Verlag} +- model: library.publisher + pk: 41 + fields: {name: Aufbau Taschenbuch Verlag} +- model: library.publisher + pk: 42 + fields: {name: Rowohlt Taschenbuch Verlag} +- model: library.book + pk: 1 + fields: + title: 2XS + publisher: null + isbn: '' + edition: 1 + pages: null + category: 1 + authors: [58] +- model: library.book + pk: 2 + fields: + title: Daemon + publisher: 42 + isbn: 978 3 499 25643 1 + edition: 1 + pages: 639 + category: 7 + authors: [105] diff --git a/django/kontor/library/migrations/0001_initial.py b/django/kontor/library/migrations/0001_initial.py new file mode 100644 index 0000000..4e0e7e6 --- /dev/null +++ b/django/kontor/library/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +<<<<<<< HEAD +# Generated by Django 1.9.6 on 2016-05-31 08:57 +======= +# Generated by Django 1.9.2 on 2016-02-16 19:01 +>>>>>>> backup +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Author', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ], + ), + migrations.CreateModel( + name='Book', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=40)), + ], + ), + ] diff --git a/django/kontor/library/migrations/0002_auto_20160624_1230.py b/django/kontor/library/migrations/0002_auto_20160624_1230.py new file mode 100644 index 0000000..db3aae0 --- /dev/null +++ b/django/kontor/library/migrations/0002_auto_20160624_1230.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-24 12:30 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ], + ), + migrations.CreateModel( + name='Edition', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ], + ), + migrations.CreateModel( + name='Publisher', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ], + ), + migrations.AddField( + model_name='book', + name='authors', + field=models.ManyToManyField(blank=True, to='library.Author'), + ), + migrations.AddField( + model_name='book', + name='isbn', + field=models.CharField(max_length=20, null=True), + ), + migrations.AddField( + model_name='book', + name='pages', + field=models.IntegerField(null=True), + ), + migrations.AddField( + model_name='book', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='library.Category'), + ), + migrations.AddField( + model_name='book', + name='edition', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='library.Edition'), + ), + migrations.AddField( + model_name='book', + name='publisher', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='library.Publisher'), + ), + ] diff --git a/django/kontor/library/migrations/0003_auto_20160624_1240.py b/django/kontor/library/migrations/0003_auto_20160624_1240.py new file mode 100644 index 0000000..72717c4 --- /dev/null +++ b/django/kontor/library/migrations/0003_auto_20160624_1240.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-24 12:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0002_auto_20160624_1230'), + ] + + operations = [ + migrations.AlterField( + model_name='book', + name='isbn', + field=models.CharField(blank=True, max_length=20, null=True), + ), + migrations.AlterField( + model_name='book', + name='pages', + field=models.IntegerField(blank=True, null=True), + ), + ] diff --git a/django/kontor/library/migrations/__init__.py b/django/kontor/library/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/kontor/library/models.py b/django/kontor/library/models.py new file mode 100644 index 0000000..b1f28cd --- /dev/null +++ b/django/kontor/library/models.py @@ -0,0 +1,51 @@ +from __future__ import unicode_literals + +from django.db import models + +# Create your models here. + + +class Author(models.Model): + name = models.CharField(max_length=40) + + def __str__(self): + return self.name + + +<<<<<<< HEAD +class Category(models.Model): + name = models.CharField(max_length=40) + + def __str__(self): + return self.name + + +class Edition(models.Model): + name = models.CharField(max_length=40) + + def __str__(self): + return self.name + + +class Publisher(models.Model): + name = models.CharField(max_length=40) + + def __str__(self): + return self.name + + +class Book(models.Model): + title = models.CharField(max_length=40) + authors = models.ManyToManyField(Author, blank=True) + publisher = models.ForeignKey(Publisher, null=True, blank=True) + isbn = models.CharField(max_length=20, null=True, blank=True) + edition = models.ForeignKey(Edition, null=True, blank=True) + pages = models.IntegerField(null=True, blank=True) + category = models.ForeignKey(Category, null=True, blank=True) +======= +class Book(models.Model): + title = models.CharField(max_length=40) +>>>>>>> backup + + def __str__(self): + return self.title diff --git a/django/kontor/library/templates/library/author_list.html b/django/kontor/library/templates/library/author_list.html new file mode 100755 index 0000000..2c64e19 --- /dev/null +++ b/django/kontor/library/templates/library/author_list.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block title %}HomeOffice{% endblock %} +{% block content %} + + + {% for author in author_list %} + + + + {% endfor %} +
Name
{{ autor.name }}
+{% endblock %} \ No newline at end of file diff --git a/django/kontor/library/templates/library/index.html b/django/kontor/library/templates/library/index.html new file mode 100755 index 0000000..8ffdead --- /dev/null +++ b/django/kontor/library/templates/library/index.html @@ -0,0 +1,2 @@ +{% extends "kontor/base.html" %} +{% block title %}Bücher{% endblock %} diff --git a/django/kontor/library/tests.py b/django/kontor/library/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/django/kontor/library/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/django/kontor/library/urls.py b/django/kontor/library/urls.py new file mode 100755 index 0000000..b400976 --- /dev/null +++ b/django/kontor/library/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls import url + +from . import views + +app_name = 'library' +urlpatterns = [ + url(r'^$', views.index, name='index'), + url(r'^author/$', views.AuthorIndexView.as_view(), name='author_list'), +] diff --git a/django/kontor/library/views.py b/django/kontor/library/views.py new file mode 100755 index 0000000..58b5aed --- /dev/null +++ b/django/kontor/library/views.py @@ -0,0 +1,46 @@ +from django.shortcuts import render +<<<<<<< HEAD +from django.http import HttpResponse +from django.views import generic +from django.contrib.auth.decorators import login_required +======= +from django.views import generic +>>>>>>> backup + +from .models import Author +from .models import Book +# Create your views here. + +<<<<<<< HEAD +@login_required +def index(request): + return render(request, 'library/index.html') +======= +>>>>>>> backup + +class AuthorIndexView(generic.ListView): + template_name = 'library/authorindex.html' + context_object_name = 'author_list' + + def get_queryset(self): + """Return the list of authors.""" + return Author.objects.all() + + +class AuthorDetailView(generic.DetailView): + model = Author + template_name = 'library/authordetail.html' + + +class BookIndexView(generic.ListView): + template_name = 'library/bookindex.html' + context_object_name = 'book_list' + + def get_queryset(self): + """Return the list of books.""" + return Book.objects.all() + + +class BookDetailView(generic.DetailView): + model = Book + template_name = 'library/bookdetail.html' diff --git a/django/kontor/manage.py b/django/kontor/manage.py new file mode 100755 index 0000000..c1f1b2c --- /dev/null +++ b/django/kontor/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "kontor.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/django/kontor/medien/__init__.py b/django/kontor/medien/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/django/kontor/medien/admin.py b/django/kontor/medien/admin.py new file mode 100755 index 0000000..8c38f3f --- /dev/null +++ b/django/kontor/medien/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/django/kontor/medien/apps.py b/django/kontor/medien/apps.py new file mode 100755 index 0000000..47c7647 --- /dev/null +++ b/django/kontor/medien/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class MedienConfig(AppConfig): + name = 'medien' diff --git a/django/kontor/medien/templates/medien/index.html b/django/kontor/medien/templates/medien/index.html new file mode 100755 index 0000000..90136c2 --- /dev/null +++ b/django/kontor/medien/templates/medien/index.html @@ -0,0 +1,2 @@ +{% extends "kontor/base.html" %} +{% block title %}Medien{% endblock %} diff --git a/django/kontor/medien/urls.py b/django/kontor/medien/urls.py new file mode 100755 index 0000000..86cfd0a --- /dev/null +++ b/django/kontor/medien/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import url + +from . import views + +app_name = 'medien' +urlpatterns = [ + url(r'^$', views.index, name='index'), +] diff --git a/django/kontor/medien/views.py b/django/kontor/medien/views.py new file mode 100755 index 0000000..cb7c6ce --- /dev/null +++ b/django/kontor/medien/views.py @@ -0,0 +1,5 @@ +from django.shortcuts import render + +# Create your views here. +def index(request): + return render(request, 'medien/index.html') diff --git a/django/kontor/static/kontor.css b/django/kontor/static/kontor.css new file mode 100644 index 0000000..c61938e --- /dev/null +++ b/django/kontor/static/kontor.css @@ -0,0 +1,109 @@ +body { + font-family: sans-serif; + color: #333333; + padding: 3em 0 4em; +} + +body, +.wrapper { + margin: 10px auto; + /*max-width: 60em;*/ +} + +header, main, section, footer { + border-radius: 0px 0.5em 0.5em; + border: 1px solid; + padding: 10px; + margin: 10px; +} + +header { + position:fixed; + top:0px; + left:0px; + right:0px; + text-align:center; + padding: 10px; + background: lightgrey; + /*border-bottom: 1px solid #d5d5d5;*/ +} + +nav { + /*position: fixed;*/ + /*padding-top: 10em; */ + + border-radius: 0px 0.5em 0.5em; + border: 1px solid; + + padding: 0; + margin: 10px; + + font-size: 0.91em; + float: left; + width: 15em; + background: lightskyblue; + border-color: skyblue; +} + +nav ul { + padding: 0; +} + +nav li { + list-style: none; + margin: 0.4em; + padding: 0; +} + +nav ul ul { + margin: 0 0 0 2em; + padding: 0; + border: none; +} + +nav ul ul li { + margin: 0.3em 0; +} + +nav a { + display: block; + padding: 0.4em; + text-decoration: none; + font-weight: bold; + border: 1px solid blue; + border-radius: 10px; + box-shadow: 0px 5px 10px white inset; + background-color: skyblue; + color: #333; + +} + +nav a:focus, +nav a:hover { + color: royalblue; + background-color: gold; +} + +main { + display: block; + background: lightblue; + border-color: #8a9da8; + margin-left: 15em; + min-width: 16em; /* Mindestbreite (der Überschrift) verhindert Anzeigefehler in modernen Browsern */ +} + +footer { + position:fixed; + padding: 10px; + margin-top: 10px; + bottom:0; + left: 0; + right:0; + background: lightgrey; + border-color: grey; +} + +footer p { + float:right; + margin: 0; +} diff --git a/django/kontor/templates/admin/base_site.html b/django/kontor/templates/admin/base_site.html new file mode 100755 index 0000000..cba9fd0 --- /dev/null +++ b/django/kontor/templates/admin/base_site.html @@ -0,0 +1,9 @@ +{% extends "admin/base.html" %} + +{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} + +{% block branding %} +

Kontor administration

+{% endblock %} + +{% block nav-global %}{% endblock %} diff --git a/django/kontor/templates/kontor/base.html b/django/kontor/templates/kontor/base.html new file mode 100755 index 0000000..db2f5e4 --- /dev/null +++ b/django/kontor/templates/kontor/base.html @@ -0,0 +1,32 @@ + + + +{% block title %}Kontor{% endblock %} +{% load staticfiles %} + + + +
Kontor
+ {% block navigation %} + + {% endblock %} +
+
+ {% block content %}{% endblock %} +
+
+ {% if user.is_authenticated %} + + {% else %} +
Login

Ingenieurbüro Thomas Peetz

+ {% endif %} + + diff --git a/django/kontor/templates/kontor/index.html b/django/kontor/templates/kontor/index.html new file mode 100755 index 0000000..22320f6 --- /dev/null +++ b/django/kontor/templates/kontor/index.html @@ -0,0 +1 @@ +{% extends "kontor/base.html" %} diff --git a/django/kontor/templates/kontor/login.html b/django/kontor/templates/kontor/login.html new file mode 100755 index 0000000..8700f8b --- /dev/null +++ b/django/kontor/templates/kontor/login.html @@ -0,0 +1,33 @@ +{% extends "kontor/base.html" %} + +{% block content %} +{% if form.errors %} +

Your username and password didn't match. Please try again.

+{% endif %} + +{% if next %} + {% if user.is_authenticated %} +

Your account doesn't have access to this page. To proceed, + please login with an account that has access.

+ {% else %} +

Please login to see this page.

+ {% endif %} +{% endif %} + +
+{% csrf_token %} + + + + + + + + + +
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
+ + + +
+{% endblock %} diff --git a/django/kontor/tradingcards/__init__.py b/django/kontor/tradingcards/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/django/kontor/tradingcards/admin.py b/django/kontor/tradingcards/admin.py new file mode 100755 index 0000000..8c38f3f --- /dev/null +++ b/django/kontor/tradingcards/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/django/kontor/tradingcards/apps.py b/django/kontor/tradingcards/apps.py new file mode 100755 index 0000000..facb92b --- /dev/null +++ b/django/kontor/tradingcards/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class TradingCardsConfig(AppConfig): + name = 'tradingcards' diff --git a/django/kontor/tradingcards/migrations/__init__.py b/django/kontor/tradingcards/migrations/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/django/kontor/tradingcards/models.py b/django/kontor/tradingcards/models.py new file mode 100755 index 0000000..bd4b2ab --- /dev/null +++ b/django/kontor/tradingcards/models.py @@ -0,0 +1,5 @@ +from __future__ import unicode_literals + +from django.db import models + +# Create your models here. diff --git a/django/kontor/tradingcards/templates/tradingcards/index.html b/django/kontor/tradingcards/templates/tradingcards/index.html new file mode 100755 index 0000000..9920067 --- /dev/null +++ b/django/kontor/tradingcards/templates/tradingcards/index.html @@ -0,0 +1,2 @@ +{% extends "kontor/base.html" %} +{% block title %}Trading Cards{% endblock %} \ No newline at end of file diff --git a/django/kontor/tradingcards/tests.py b/django/kontor/tradingcards/tests.py new file mode 100755 index 0000000..7ce503c --- /dev/null +++ b/django/kontor/tradingcards/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/django/kontor/tradingcards/urls.py b/django/kontor/tradingcards/urls.py new file mode 100755 index 0000000..a3780aa --- /dev/null +++ b/django/kontor/tradingcards/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url + +from . import views + +urlpatterns = [ + url(r'^$', views.index, name='index'), +] diff --git a/django/kontor/tradingcards/views.py b/django/kontor/tradingcards/views.py new file mode 100755 index 0000000..4aa36e4 --- /dev/null +++ b/django/kontor/tradingcards/views.py @@ -0,0 +1,5 @@ +from django.shortcuts import render + +# Create your views here. +def index(request): + return render(request, 'tradingcards/index.html') diff --git a/django/kontor/tysc/__init__.py b/django/kontor/tysc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/kontor/tysc/admin.py b/django/kontor/tysc/admin.py new file mode 100644 index 0000000..024e870 --- /dev/null +++ b/django/kontor/tysc/admin.py @@ -0,0 +1,22 @@ +from django.contrib import admin + +from .models import Sport +from .models import Team +from .models import Position +from .models import Player +from .models import Manufacturer +from .models import Serie +from .models import InsertSet +from .models import ParallelSet +from .models import SportCard +# Register your models here. + +admin.site.register(Sport) +admin.site.register(Team) +admin.site.register(Position) +admin.site.register(Player) +admin.site.register(Manufacturer) +admin.site.register(Serie) +admin.site.register(InsertSet) +admin.site.register(ParallelSet) +admin.site.register(SportCard) \ No newline at end of file diff --git a/django/kontor/tysc/apps.py b/django/kontor/tysc/apps.py new file mode 100644 index 0000000..95ecaae --- /dev/null +++ b/django/kontor/tysc/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class TyscConfig(AppConfig): + name = 'tysc' diff --git a/django/kontor/tysc/fixtures/teams.yaml b/django/kontor/tysc/fixtures/teams.yaml new file mode 100755 index 0000000..b07dd35 --- /dev/null +++ b/django/kontor/tysc/fixtures/teams.yaml @@ -0,0 +1,561 @@ +- model: tysc.sport + pk: 1 + fields: {name: Football} +- model: tysc.sport + pk: 2 + fields: {name: Baseball} +- model: tysc.sport + pk: 3 + fields: {name: Basketball} +- model: tysc.sport + pk: 4 + fields: {name: Hockey} +- model: tysc.team + pk: 1 + fields: {name: Buffalo Bills, shortname: Bills, sport: 1} +- model: tysc.team + pk: 2 + fields: {name: Indianapolis Colts, shortname: Colts, sport: 1} +- model: tysc.team + pk: 3 + fields: {name: Miami Dolphins, shortname: Dolphins, sport: 1} +- model: tysc.team + pk: 4 + fields: {name: New England Patriots, shortname: Patriots, sport: 1} +- model: tysc.team + pk: 5 + fields: {name: New York Jets, shortname: Jets, sport: 1} +- model: tysc.team + pk: 6 + fields: {name: Baltimore Ravens, shortname: Ravens, sport: 1} +- model: tysc.team + pk: 7 + fields: {name: Cincinnati Bengals, shortname: Bengals, sport: 1} +- model: tysc.team + pk: 8 + fields: {name: Cleveland Browns, shortname: Browns, sport: 1} +- model: tysc.team + pk: 9 + fields: {name: Jacksonville Jaguars, shortname: Jaguars, sport: 1} +- model: tysc.team + pk: 10 + fields: {name: Pittsburgh Steelers, shortname: Steelers, sport: 1} +- model: tysc.team + pk: 11 + fields: {name: Tennessee Titans, shortname: Titans, sport: 1} +- model: tysc.team + pk: 12 + fields: {name: Denver Broncos, shortname: Broncos, sport: 1} +- model: tysc.team + pk: 13 + fields: {name: Kansas City Chiefs, shortname: Chiefs, sport: 1} +- model: tysc.team + pk: 14 + fields: {name: Oakland Raiders, shortname: Raiders, sport: 1} +- model: tysc.team + pk: 15 + fields: {name: San Diego Chargers, shortname: Chargers, sport: 1} +- model: tysc.team + pk: 16 + fields: {name: Seattle Seahawks, shortname: Seahawks, sport: 1} +- model: tysc.team + pk: 17 + fields: {name: Arizona Cardinals, shortname: Cardinals, sport: 1} +- model: tysc.team + pk: 18 + fields: {name: Dallas Cowboys, shortname: Cowboys, sport: 1} +- model: tysc.team + pk: 19 + fields: {name: New York Giants, shortname: Giants, sport: 1} +- model: tysc.team + pk: 20 + fields: {name: Philadelphia Eagles, shortname: Eagles, sport: 1} +- model: tysc.team + pk: 21 + fields: {name: Washington Redskins, shortname: Redskins, sport: 1} +- model: tysc.team + pk: 22 + fields: {name: Chicago Bears, shortname: Bears, sport: 1} +- model: tysc.team + pk: 23 + fields: {name: Detroit Lions, shortname: Lions, sport: 1} +- model: tysc.team + pk: 24 + fields: {name: Green Bay Packers, shortname: Packers, sport: 1} +- model: tysc.team + pk: 25 + fields: {name: Minnesota Vikings, shortname: Vikings, sport: 1} +- model: tysc.team + pk: 26 + fields: {name: Tampa Bay Buccaneers, shortname: Buccaneers, sport: 1} +- model: tysc.team + pk: 27 + fields: {name: Atlanta Falcons, shortname: Falcons, sport: 1} +- model: tysc.team + pk: 28 + fields: {name: Carolina Panthers, shortname: Panthers, sport: 1} +- model: tysc.team + pk: 29 + fields: {name: New Orleans Saints, shortname: Saints, sport: 1} +- model: tysc.team + pk: 30 + fields: {name: St.Louis Rams, shortname: Rams, sport: 1} +- model: tysc.team + pk: 31 + fields: {name: San Francisco 49ers, shortname: 49ers, sport: 1} +- model: tysc.team + pk: 32 + fields: {name: Baltimore Orioles, shortname: Orioles, sport: 2} +- model: tysc.team + pk: 33 + fields: {name: Boston Red Sox, shortname: Red Sox, sport: 2} +- model: tysc.team + pk: 34 + fields: {name: New York Yankees, shortname: Yankees, sport: 2} +- model: tysc.team + pk: 35 + fields: {name: Tampa Bay Devil Rays, shortname: Devil Rays, sport: 2} +- model: tysc.team + pk: 36 + fields: {name: Toronto Blue Jays, shortname: Blue Jays, sport: 2} +- model: tysc.team + pk: 37 + fields: {name: Chicago White Sox, shortname: White Sox, sport: 2} +- model: tysc.team + pk: 38 + fields: {name: Cleveland Indians, shortname: Indians, sport: 2} +- model: tysc.team + pk: 39 + fields: {name: Detroit Tigers, shortname: Tigers, sport: 2} +- model: tysc.team + pk: 40 + fields: {name: Kansas City Royals, shortname: Royals, sport: 2} +- model: tysc.team + pk: 41 + fields: {name: Minnesota Twins, shortname: Twins, sport: 2} +- model: tysc.team + pk: 42 + fields: {name: Anaheim Angels, shortname: Angels, sport: 2} +- model: tysc.team + pk: 43 + fields: {name: Oakland Athletics, shortname: Athletics, sport: 2} +- model: tysc.team + pk: 44 + fields: {name: Seattle Mariners, shortname: Mariners, sport: 2} +- model: tysc.team + pk: 45 + fields: {name: Texas Rangers, shortname: Rangers, sport: 2} +- model: tysc.team + pk: 46 + fields: {name: Atlanta Braves, shortname: Braves, sport: 2} +- model: tysc.team + pk: 47 + fields: {name: Florida Marlins, shortname: Marlins, sport: 2} +- model: tysc.team + pk: 48 + fields: {name: Montreal Expos, shortname: Expos, sport: 2} +- model: tysc.team + pk: 49 + fields: {name: New York Mets, shortname: Mets, sport: 2} +- model: tysc.team + pk: 50 + fields: {name: Philadelphia Phillies, shortname: Phillies, sport: 2} +- model: tysc.team + pk: 51 + fields: {name: Chicago Cubs, shortname: Cubs, sport: 2} +- model: tysc.team + pk: 52 + fields: {name: Cincinnati Reds, shortname: Reds, sport: 2} +- model: tysc.team + pk: 53 + fields: {name: Houston Astros, shortname: Astros, sport: 2} +- model: tysc.team + pk: 54 + fields: {name: Milwaukee Brewers, shortname: Brewers, sport: 2} +- model: tysc.team + pk: 55 + fields: {name: Pittsburgh Pirates, shortname: Pirates, sport: 2} +- model: tysc.team + pk: 56 + fields: {name: St.Louis Cardinals, shortname: Cardinals, sport: 2} +- model: tysc.team + pk: 57 + fields: {name: Arizona Diamondbacks, shortname: Diamondbacks, sport: 2} +- model: tysc.team + pk: 58 + fields: {name: Colorado Rockies, shortname: Rockies, sport: 2} +- model: tysc.team + pk: 59 + fields: {name: Los Angeles Dodgers, shortname: Dodgers, sport: 2} +- model: tysc.team + pk: 60 + fields: {name: San Diego Padres, shortname: Padres, sport: 2} +- model: tysc.team + pk: 61 + fields: {name: San Francisco Giants, shortname: Giants, sport: 2} +- model: tysc.team + pk: 62 + fields: {name: Boston Celtics, shortname: Celtics, sport: 3} +- model: tysc.team + pk: 63 + fields: {name: Miami Heat, shortname: Heat, sport: 3} +- model: tysc.team + pk: 64 + fields: {name: New Jersey Nets, shortname: Mets, sport: 3} +- model: tysc.team + pk: 65 + fields: {name: New York Knicks, shortname: Knicks, sport: 3} +- model: tysc.team + pk: 66 + fields: {name: Orlando Magic, shortname: Magic, sport: 3} +- model: tysc.team + pk: 67 + fields: {name: Philadelphia 76ers, shortname: 76ers, sport: 3} +- model: tysc.team + pk: 68 + fields: {name: Washington Wizards, shortname: Wizards, sport: 3} +- model: tysc.team + pk: 69 + fields: {name: Atlanta Hawks, shortname: Hawks, sport: 3} +- model: tysc.team + pk: 70 + fields: {name: Charlotte Hornets, shortname: Hornets, sport: 3} +- model: tysc.team + pk: 71 + fields: {name: Chicago Bulls, shortname: Bulls, sport: 3} +- model: tysc.team + pk: 72 + fields: {name: Cleveland Cavaliers, shortname: Cavaliers, sport: 3} +- model: tysc.team + pk: 73 + fields: {name: Detroit Pistons, shortname: Pistons, sport: 3} +- model: tysc.team + pk: 74 + fields: {name: Indiana Pacers, shortname: Pacers, sport: 3} +- model: tysc.team + pk: 75 + fields: {name: Milwaukee Bucks, shortname: Bucks, sport: 3} +- model: tysc.team + pk: 76 + fields: {name: Toronto Raptors, shortname: Raptors, sport: 3} +- model: tysc.team + pk: 77 + fields: {name: Dallas Mavericks, shortname: Mavericks, sport: 3} +- model: tysc.team + pk: 78 + fields: {name: Denver Nuggets, shortname: Nuggets, sport: 3} +- model: tysc.team + pk: 79 + fields: {name: Houston Rockets, shortname: Rockets, sport: 3} +- model: tysc.team + pk: 80 + fields: {name: Minnesota Timberwolves, shortname: Timberwolves, sport: 3} +- model: tysc.team + pk: 81 + fields: {name: San Antonio Spurs, shortname: Spurs, sport: 3} +- model: tysc.team + pk: 82 + fields: {name: Utah Jazz, shortname: Jazz, sport: 3} +- model: tysc.team + pk: 83 + fields: {name: Vancouver Grizzlies, shortname: Grizzlies, sport: 3} +- model: tysc.team + pk: 84 + fields: {name: Golden State Warriors, shortname: Warriors, sport: 4} +- model: tysc.team + pk: 85 + fields: {name: Los Angeles Clippers, shortname: Clippers, sport: 3} +- model: tysc.team + pk: 86 + fields: {name: Los Angeles Lakers, shortname: Lakers, sport: 3} +- model: tysc.team + pk: 87 + fields: {name: Phoenix Suns, shortname: Suns, sport: 3} +- model: tysc.team + pk: 88 + fields: {name: Portland Trail Blazers, shortname: Blazers, sport: 3} +- model: tysc.team + pk: 89 + fields: {name: Sacramento Kings, shortname: Kings, sport: 3} +- model: tysc.team + pk: 90 + fields: {name: Seattle SuperSonics, shortname: SuperSonics, sport: 3} +- model: tysc.team + pk: 91 + fields: {name: Boston Bruins, shortname: Bruins, sport: 4} +- model: tysc.team + pk: 92 + fields: {name: Buffalo Sabres, shortname: Sabres, sport: 4} +- model: tysc.team + pk: 93 + fields: {name: Montreal Canadiens, shortname: Canadiens, sport: 4} +- model: tysc.team + pk: 94 + fields: {name: Ottawa Senators, shortname: Senators, sport: 4} +- model: tysc.team + pk: 95 + fields: {name: Toronto Maple Leafs, shortname: Maple Leafs, sport: 4} +- model: tysc.team + pk: 96 + fields: {name: New Jersey Devils, shortname: Devils, sport: 4} +- model: tysc.team + pk: 97 + fields: {name: New York Islander, shortname: Islander, sport: 4} +- model: tysc.team + pk: 98 + fields: {name: New York Rangers, shortname: Rangers, sport: 4} +- model: tysc.team + pk: 99 + fields: {name: Philadelphia Flyers, shortname: Flyers, sport: 4} +- model: tysc.team + pk: 100 + fields: {name: Pittsburgh Penguins, shortname: Penguins, sport: 4} +- model: tysc.team + pk: 101 + fields: {name: Atlanta Trashers, shortname: Trashers, sport: 4} +- model: tysc.team + pk: 102 + fields: {name: Carolina Hurricanes, shortname: Hurricanes, sport: 4} +- model: tysc.team + pk: 103 + fields: {name: Florida Panthers, shortname: Panthers, sport: 4} +- model: tysc.team + pk: 104 + fields: {name: Tampa Bay Lightnings, shortname: Lightnings, sport: 4} +- model: tysc.team + pk: 105 + fields: {name: Washington Capitals, shortname: Capitals, sport: 4} +- model: tysc.team + pk: 106 + fields: {name: Chicago Blackhawks, shortname: Blackhawks, sport: 4} +- model: tysc.team + pk: 107 + fields: {name: Columbo Blue Jackets, shortname: Blue Jackets, sport: 4} +- model: tysc.team + pk: 108 + fields: {name: Detroit Red Wings, shortname: Red Wings, sport: 4} +- model: tysc.team + pk: 109 + fields: {name: Nashville Predators, shortname: Predators, sport: 4} +- model: tysc.team + pk: 110 + fields: {name: St.Louis Blues, shortname: Blues, sport: 4} +- model: tysc.team + pk: 111 + fields: {name: Calgary Flames, shortname: Flames, sport: 4} +- model: tysc.team + pk: 112 + fields: {name: Colorado Avalanche, shortname: Avalanche, sport: 4} +- model: tysc.team + pk: 113 + fields: {name: Edmonton Oilers, shortname: Oilers, sport: 4} +- model: tysc.team + pk: 114 + fields: {name: Minnesota Wild, shortname: Wild, sport: 4} +- model: tysc.team + pk: 115 + fields: {name: Vancouver Canucks, shortname: Canucks, sport: 4} +- model: tysc.team + pk: 116 + fields: {name: Anaheim Mighty Ducks, shortname: Mighty Ducks, sport: 4} +- model: tysc.team + pk: 117 + fields: {name: Dallas Stars, shortname: Stars, sport: 4} +- model: tysc.team + pk: 118 + fields: {name: Los Angeles Kings, shortname: Kings, sport: 4} +- model: tysc.team + pk: 119 + fields: {name: Phoenix Coyotes, shortname: Coyotes, sport: 4} +- model: tysc.team + pk: 120 + fields: {name: San Jose Sharks, shortname: Sharks, sport: 4} +- model: tysc.team + pk: 121 + fields: {name: Houston Texans, shortname: Texans, sport: 1} +- model: tysc.team + pk: 122 + fields: {name: Houston Oilers, shortname: Oilers, sport: 1} +- model: tysc.position + pk: 1 + fields: {name: QB, description: Quarterback, sport: 1} +- model: tysc.position + pk: 2 + fields: {name: WR, description: Wide Receiver, sport: 1} +- model: tysc.position + pk: 3 + fields: {name: RB, description: Running Back, sport: 1} +- model: tysc.position + pk: 4 + fields: {name: LB, description: Linebacker, sport: 1} +- model: tysc.position + pk: 5 + fields: {name: TE, description: Tight End, sport: 1} +- model: tysc.position + pk: 6 + fields: {name: FB, description: Fullback, sport: 1} +- model: tysc.position + pk: 7 + fields: {name: SS, description: Strong Safety, sport: 1} +- model: tysc.position + pk: 8 + fields: {name: DE, description: Defensive End, sport: 1} +- model: tysc.position + pk: 9 + fields: {name: K, description: Kicker, sport: 1} +- model: tysc.position + pk: 10 + fields: {name: P, description: Punter, sport: 1} +- model: tysc.position + pk: 11 + fields: {name: LG, description: Left Guard, sport: 1} +- model: tysc.position + pk: 12 + fields: {name: RG, description: Right Guard, sport: 1} +- model: tysc.position + pk: 13 + fields: {name: OF, description: OF, sport: 1} +- model: tysc.position + pk: 14 + fields: {name: DB, description: Defensive Back, sport: 1} +- model: tysc.position + pk: 15 + fields: {name: CB, description: Corner Back, sport: 1} +- model: tysc.position + pk: 16 + fields: {name: C, description: Center, sport: 2} +- model: tysc.position + pk: 17 + fields: {name: 1B, description: First Base, sport: 2} +- model: tysc.position + pk: 18 + fields: {name: 2B, description: Second Base, sport: 2} +- model: tysc.position + pk: 19 + fields: {name: 3B, description: Third Base, sport: 2} +- model: tysc.position + pk: 20 + fields: {name: SS, description: SS, sport: 2} +- model: tysc.position + pk: 21 + fields: {name: LF, description: Left Field, sport: 2} +- model: tysc.position + pk: 22 + fields: {name: CF, description: Center Field, sport: 2} +- model: tysc.position + pk: 23 + fields: {name: RF, description: Right Field, sport: 2} +- model: tysc.position + pk: 24 + fields: {name: DH, description: DH, sport: 2} +- model: tysc.position + pk: 25 + fields: {name: P, description: Pitcher, sport: 2} +- model: tysc.player + pk: 1 + fields: {name: 'Pathon, Jerome'} +- model: tysc.player + pk: 2 + fields: {name: 'Bruschi, Tedy'} +- model: tysc.player + pk: 3 + fields: {name: 'Couch, Tim'} +- model: tysc.player + pk: 4 + fields: {name: 'Shea, Aaron'} +- model: tysc.player + pk: 5 + fields: {name: 'Lewis, Jamal'} +- model: tysc.player + pk: 6 + fields: {name: 'Lewis, Jermaine'} +- model: tysc.player + pk: 7 + fields: {name: 'Banks, Tony'} +- model: tysc.player + pk: 8 + fields: {name: "Fuamatu-Ma'Afala, Chris"} +- model: tysc.player + pk: 9 + fields: {name: 'Bettis, Jerome'} +- model: tysc.player + pk: 10 + fields: {name: 'Stewart, Kordell'} +- model: tysc.player + pk: 11 + fields: {name: 'Moon, Warren'} +- model: tysc.player + pk: 12 + fields: {name: 'Lockett, Kevin'} +- model: tysc.player + pk: 13 + fields: {name: 'Gannon, Rich'} +- model: tysc.player + pk: 14 + fields: {name: 'Jett, James'} +- model: tysc.player + pk: 15 + fields: {name: 'Strong, Mack'} +- model: tysc.player + pk: 16 + fields: {name: 'Huard, Brock'} +- model: tysc.player + pk: 17 + fields: {name: 'Watters, Ricky'} +- model: tysc.player + pk: 18 + fields: {name: 'Aikman, Troy'} +- model: tysc.player + pk: 19 + fields: {name: 'LaFleur, David'} +- model: tysc.player + pk: 20 + fields: {name: 'Brazzell, Chris'} +- model: tysc.player + pk: 21 + fields: {name: 'Dayne, Ron'} +- model: tysc.player + pk: 22 + fields: {name: 'Brown, Na'} +- model: tysc.player + pk: 23 + fields: {name: 'Small, Torrance'} +- model: tysc.player + pk: 24 + fields: {name: 'Lewis, Chad'} +- model: tysc.player + pk: 25 + fields: {name: 'Murrell, Adrian'} +- model: tysc.player + pk: 26 + fields: {name: 'Smith, Maurice'} +- model: tysc.player + pk: 27 + fields: {name: 'Chandler, Chris'} +- model: tysc.player + pk: 28 + fields: {name: 'Kanell, Danny'} +- model: tysc.player + pk: 29 + fields: {name: 'Williams, Ricky'} +- model: tysc.player + pk: 30 + fields: {name: 'Garcia, Jeff'} +- model: tysc.player + pk: 31 + fields: {name: 'Streets, Tai'} +- model: tysc.player + pk: 32 + fields: {name: 'Garner, Charlie'} +- model: tysc.player + pk: 33 + fields: {name: 'Rice, Jerry'} +- model: tysc.player + pk: 34 + fields: {name: 'Owens, Terrell'} +- model: tysc.player + pk: 35 + fields: {name: 'Bruce, Isaac'} +- model: tysc.player + pk: 36 + fields: {name: 'Canidate, Trung'} diff --git a/django/kontor/tysc/migrations/0001_initial.py b/django/kontor/tysc/migrations/0001_initial.py new file mode 100644 index 0000000..f4d3882 --- /dev/null +++ b/django/kontor/tysc/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-02-15 09:56 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Sport', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ], + ), + migrations.CreateModel( + name='Team', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=60)), + ('shortname', models.CharField(max_length=30)), + ('sport', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tysc.Sport')), + ], + ), + ] diff --git a/django/kontor/tysc/migrations/0002_auto_20160216_2201.py b/django/kontor/tysc/migrations/0002_auto_20160216_2201.py new file mode 100644 index 0000000..e2df575 --- /dev/null +++ b/django/kontor/tysc/migrations/0002_auto_20160216_2201.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-02-16 22:01 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tysc', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='InsertSet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ], + ), + migrations.CreateModel( + name='Manufacturer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ], + ), + migrations.CreateModel( + name='ParallelSet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ('manufacturer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tysc.Manufacturer')), + ], + ), + migrations.CreateModel( + name='Player', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ], + ), + migrations.CreateModel( + name='Position', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=4)), + ('description', models.CharField(max_length=30)), + ('sport', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tysc.Sport')), + ], + ), + migrations.CreateModel( + name='Serie', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=40)), + ('manufacturer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tysc.Manufacturer')), + ], + ), + migrations.CreateModel( + name='SportCard', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('rookie', models.BooleanField()), + ('year', models.IntegerField()), + ('number', models.IntegerField()), + ('insert_set', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='tysc.InsertSet')), + ('manufacturer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tysc.Manufacturer')), + ('parallel_set', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='tysc.ParallelSet')), + ('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tysc.Player')), + ('serie', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='tysc.Serie')), + ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tysc.Team')), + ], + ), + migrations.AddField( + model_name='insertset', + name='manufacturer', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tysc.Manufacturer'), + ), + ] diff --git a/django/kontor/tysc/migrations/__init__.py b/django/kontor/tysc/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/kontor/tysc/models.py b/django/kontor/tysc/models.py new file mode 100644 index 0000000..10d3eb5 --- /dev/null +++ b/django/kontor/tysc/models.py @@ -0,0 +1,83 @@ +from __future__ import unicode_literals + +from django.db import models + +# Create your models here. + + +class Sport(models.Model): + name = models.CharField(max_length=40) + + def __str__(self): + return self.name + + +class Position(models.Model): + name = models.CharField(max_length=4) + description = models.CharField(max_length=30) + sport = models.ForeignKey(Sport) + + def __str__(self): + return self.name + '(' + self.description + ')' + + +class Team(models.Model): + name = models.CharField(max_length=60) + shortname = models.CharField(max_length=30) + sport = models.ForeignKey(Sport) + + def __str__(self): + return self.name + + +class Player(models.Model): + name = models.CharField(max_length=40) + + def __str__(self): + return self.name + + +class Manufacturer(models.Model): + name = models.CharField(max_length=40) + + def __str__(self): + return self.name + + +class Serie(models.Model): + name = models.CharField(max_length=40) + manufacturer = models.ForeignKey(Manufacturer) + + def __str__(self): + return self.name + + +class InsertSet(models.Model): + name = models.CharField(max_length=40) + manufacturer = models.ForeignKey(Manufacturer) + + def __str__(self): + return self.name + + +class ParallelSet(models.Model): + name = models.CharField(max_length=40) + manufacturer = models.ForeignKey(Manufacturer) + + def __str__(self): + return self.name + + +class SportCard(models.Model): + player = models.ForeignKey(Player) + team = models.ForeignKey(Team) + manufacturer = models.ForeignKey(Manufacturer) + serie = models.ForeignKey(Serie, null=True) + parallel_set = models.ForeignKey(ParallelSet, null=True) + insert_set = models.ForeignKey(InsertSet, null=True) + rookie = models.BooleanField() + year = models.IntegerField() + number = models.IntegerField() + + def __str__(self): + return str(self.number) diff --git a/django/kontor/tysc/templates/tysc/index.html b/django/kontor/tysc/templates/tysc/index.html new file mode 100755 index 0000000..7cfcf38 --- /dev/null +++ b/django/kontor/tysc/templates/tysc/index.html @@ -0,0 +1,2 @@ +{% extends "kontor/base.html" %} +{% block title %}TradeYourSportsCards{% endblock %} diff --git a/django/kontor/tysc/tests.py b/django/kontor/tysc/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/django/kontor/tysc/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/django/kontor/tysc/urls.py b/django/kontor/tysc/urls.py new file mode 100755 index 0000000..e243e6e --- /dev/null +++ b/django/kontor/tysc/urls.py @@ -0,0 +1,11 @@ +from django.conf.urls import url + +from . import views + +urlpatterns = [ + url(r'^$', views.index, name='index'), +] +<<<<<<< HEAD +======= + +>>>>>>> backup diff --git a/django/kontor/tysc/views.py b/django/kontor/tysc/views.py new file mode 100755 index 0000000..632d963 --- /dev/null +++ b/django/kontor/tysc/views.py @@ -0,0 +1,13 @@ +from django.shortcuts import render +from django.http import HttpResponse + +# Create your views here. + +<<<<<<< HEAD +def index(request): + return render(request, 'tysc/index.html') +======= + +def index(request): + return HttpResponse('You are in the TYSC application') +>>>>>>> backup diff --git a/go/.env b/go/.env new file mode 100644 index 0000000..33a4c5f --- /dev/null +++ b/go/.env @@ -0,0 +1,10 @@ +APP_NAME="Kontor" +APP_VERSION="v0.1.0" + +# HTTP Response Content-Type Header - Success +HTTP_CONTENT_TYPE="application/vnd.api+json" + +# HTTP Response Content-Type Header - Error +HTTP_PROBLEM="application/problem+json" + +HTTP_PORT=":8086" diff --git a/go/.gitignore b/go/.gitignore new file mode 100644 index 0000000..0303555 --- /dev/null +++ b/go/.gitignore @@ -0,0 +1,9 @@ +.gradle/ +docs/build/ +bin/ +build/ +cover.out +coverage.html +coverage.xml +.settings/ +.project diff --git a/go/.gitlab-ci.yml b/go/.gitlab-ci.yml new file mode 100644 index 0000000..1580961 --- /dev/null +++ b/go/.gitlab-ci.yml @@ -0,0 +1,116 @@ +variables: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + +before_script: + - source "/home/gitlab-runner/.sdkman/bin/sdkman-init.sh" + - sdk update + - sdk d java 11.0.12-open + +stages: +- environment +- prepare +- build +- test +- analysis +- deploy + +environment: + stage: environment + script: + - echo "PATH=$PATH:/usr/local/go/bin:/home/gitlab-runner/go/bin" >> build.env + artifacts: + reports: + dotenv: build.env + +Prepare Go dependencies: + stage: prepare + script: + - go get -u github.com/spf13/cobra + - go get -u github.com/jstemmer/go-junit-report + - go get -u github.com/inconshreveable/mousetrap + - go get -u github.com/mitchellh/go-homedir + - go get github.com/boumenot/gocover-cobertura + - go get -u gotest.tools/gotestsum + - go get -u github.com/cryptix/wav + - go get -u golang.org/x/lint/golint + dependencies: + - environment + +Create Documentation: + stage: build + script: + - chmod +x docs/gradlew + - cd docs; ./gradlew publish + +Compile Go Application: + stage: build + script: make build + +Test Go Application: + stage: test + script: + - gotestsum --junitfile report.xml --format testname -- -coverprofile=coverage.txt -covermode count ./... + - go vet ./... 2> govet-report.out + - gocover-cobertura < coverage.txt > coverage.xml + - golint ./... > golint-report.out + dependencies: + - environment + artifacts: + when: always + paths: + - coverage.xml + - report.xml + reports: + junit: report.xml + coverage_report: + coverage_format: cobertura + path: coverage.xml + +Code analysis: + stage: analysis + script: + - go test -coverprofile cover.out ./... -json > report.json + - go vet ./... 2> govet-report.out + - golint ./... > golint-report.out + - echo "sonar.projectKey=kontor_kontor-go_AX-cQT62rXuu6JVRvr-z" >> sonar-project.properties + - echo "sonar.sources=." >> sonar-project.properties + - echo "sonar.exclusions=**/*_test.go" >> sonar-project.properties + - echo "sonar.tests=." >> sonar-project.properties + - echo "sonar.test.inclusions=**/*_test.go" >> sonar-project.properties + - echo "sonar.go.tests.reportPaths=report.json" >> sonar-project.properties + - echo "sonar.go.coverage.reportPaths=cover.out" >> sonar-project.properties + - echo "sonar.go.govet.reportPaths=govet-report.out" >> sonar-project.properties + - echo "sonar.go.golint.reportPaths=golint-report.out" >> sonar-project.properties + - /data/sonar-scanner/bin/sonar-scanner -Dsonar.projectVersion=$(git describe --abbrev=0) -Dsonar.host.url=https://sonar.thpeetz.de -Dsonar.login=319616a2761ac3e96a1c7aacc54976bfff4096a9 + dependencies: + - environment + +Deploy To Staging: + stage: deploy + script: + - make build + - ssh kontor /home/kontor/kontor-test_service stop + - rsync -av templates kontor:/home/kontor/staging + - rsync -av bin/kontor kontor:/home/kontor/staging + - ssh kontor /home/kontor/kontor-test_service start + environment: + name: staging + url: https://kontor-test.thpeetz.de + only: + - main + +Deploy to Production: + stage: deploy + script: + - make build + - sudo service kontor stop + - rsync -av templates kontor:/home/kontor/production + - rsync -av bin/kontor kontor:/home/kontor/production + - sudo service kontor start + environment: + name: production + url: https://kontor.thpeetz.de + + only: + - main + when: manual diff --git a/go/Makefile b/go/Makefile new file mode 100644 index 0000000..7f29013 --- /dev/null +++ b/go/Makefile @@ -0,0 +1,73 @@ +# Go parameters +GOPATH=$(HOME)/go +#GOBIN=$(shell pwd)/bin +GOBIN=/usr/local/go/bin +GOCMD=$(GOBIN)/go +GOGET=$(GOCMD) get +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean +GOTEST=$(GOCMD) test +GOCOVER=$(GOCMD) tool cover +COBERTURA=$(HOME)/go/bin/gocover-cobertura +GOLINT=$(GOBIN)/golint +GOVET=$(GOCMD) vet + +# Project parameters +GONAME=kontor +GOFILE=cmd/kontor/main.go + +#.PHONY: all deps clean clean-bin clean-doc + +all: deps build + +deps: + $(GOGET) -u -v github.com/jstemmer/go-junit-report + $(GOGET) -u -v github.com/tebeka/go2xunit + $(GOGET) -u -v github.com/t-yuki/gocover-cobertura + $(GOGET) -u -v github.com/spf13/cobra/cobra + $(GOGET) -u -v github.com/inconshreveable/mousetrap + $(GOGET) -u -v github.com/mitchellh/go-homedir + $(GOGET) -u -v github.com/golang/protobuf/proto + $(GOGET) -u -v github.com/gin-gonic/gin + $(GOGET) -u -v github.com/gin-gonic/gin/binding + $(GOGET) -u -v github.com/gin-gonic/gin/render + $(GOGET) -u -v github.com/gin-contrib/sse + $(GOGET) -u -v github.com/mattn/go-isatty + $(GOGET) -u -v github.com/ugorji/go/codec + $(GOGET) -u -v golang.org/x/crypto/bcrypt + $(GOGET) -u -v golang.org/x/crypto/blowfish + G$(GOGET) -u -v gopkg.in/yaml.v2 + $(GOGET) -u -v gopkg.in/mgo.v2 + $(GOGET) -u -v gopkg.in/mgo.v2/bson + +build: bin/$(GONAME) + +bin/$(GONAME): $(GOFILE) + @echo "Building $(GOFILE) to ./bin" + $(GOBUILD) -v -ldflags="-X main.version=$(shell git describe --always --long --dirty)" -o bin/$(GONAME) $(GOFILE) + +install: + @echo using $(GOPATH) + $(GOCMD) install -v -ldflags="-X main.version=$(shell git describe --always --long --dirty)" ./... + +test: + $(GOTEST) -v ./... + $(GOTEST) -coverprofile=cover.out ./... + $(GOCOVER) -html=cover.out -o coverage.html + $(COBERTURA) < cover.out > coverage.xml + $(GOLINT) ./... + $(GOVET) -v ./... + +doc: + cd docs; ./gradlew build + +clean: clean-doc clean-bin + + +clean-doc: + @echo "Cleaning Gradle build" + cd docs; ./gradlew clean + +clean-bin: + @echo "Cleaning Go build" + rm -rf bin/ diff --git a/go/README.md b/go/README.md new file mode 100644 index 0000000..6814615 --- /dev/null +++ b/go/README.md @@ -0,0 +1,4 @@ +[![pipeline status](https://gitlab.thpeetz.de/kontor/kontor-go/badges/master/pipeline.svg)](https://gitlab.thpeetz.de/kontor/kontor-go/commits/master) +[![coverage report](https://gitlab.thpeetz.de/kontor/kontor-go/badges/master/coverage.svg)](https://gitlab.thpeetz.de/kontor/kontor-go/commits/master) + +# Kontor diff --git a/go/cmd/kontor/main.go b/go/cmd/kontor/main.go new file mode 100644 index 0000000..c2dbeeb --- /dev/null +++ b/go/cmd/kontor/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "database/sql" + "log" + "time" + + _ "github.com/go-sql-driver/mysql" +) + +func main() { + connectionString := "kontor:kontor@tcp(127.0.0.1:3306)/kontor?parseTime=true" + db, err := sql.Open("mysql", connectionString) + if err != nil { + log.Printf("setup database error: %v", err) + return + } + db.SetConnMaxLifetime(time.Minute * 3) + db.SetMaxOpenConns(10) + db.SetMaxIdleConns(10) + log.Println("Database connected") + + rows, err := db.Query("SELECT id, created_date, last_modified_date, version, url, title, review FROM media_file") + if err != nil { + log.Fatal((err.Error())) + } + defer rows.Close() + var rec MediaFile + for rows.Next() { + err = rows.Scan(&rec.ID, &rec.CreatedDate, &rec.LastModifiedDate, &rec.Version, &rec.Url, &rec.Title, &rec.Review) + if err != nil { + log.Fatal(err.Error()) + } + log.Println(rec, string(rec.Url), string(rec.Title)) + } +} diff --git a/go/cmd/kontor/model.go b/go/cmd/kontor/model.go new file mode 100644 index 0000000..b7634c5 --- /dev/null +++ b/go/cmd/kontor/model.go @@ -0,0 +1,25 @@ +package main + +import ( + "time" + + "github.com/google/uuid" +) + +type AbstractEntity struct { + ID uuid.UUID `gorm:"type:uuid;primary_key" json:"id"` + Version uint `json:"version"` + CreatedDate time.Time `json:"createdDate"` + LastModifiedDate time.Time `json:"lastModifiedDate"` +} + +type MediaFile struct { + AbstractEntity + Url []byte `json:"url"` + Review []uint8 `json:"review"` + ShouldDownload []uint8 `json:"shouldDownload"` + Title []byte `json:"title"` + CloudLink string `json:"cloudLink"` + FileName string `json:"fileName"` + Path string `json:"path"` +} diff --git a/go/cmd/root.go b/go/cmd/root.go new file mode 100644 index 0000000..9a1c5e2 --- /dev/null +++ b/go/cmd/root.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "fmt" + "log" + "os" + + "gitlab.thpeetz.de/kontor/kontor-go/pkg/properties" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/setup" + + "github.com/gin-gonic/gin" + "github.com/spf13/cobra" +) + +var ( + // Verbose defines the parameter verbose. + Verbose bool + // Version defines the version of the web application. + Version string + // Debug defines the debug parameter. + Debug bool + // Port defines the parameter port. + Port int + // TemplatesDir defines the root directory of template files. + TemplatesDir string + router *gin.Engine +) + +var rootCmd = &cobra.Command{ + Use: "kontor", + Short: "kontor", + Long: `kontor`, + Run: func(cmd *cobra.Command, args []string) { + // Set Gin to production mode + if Debug { + gin.SetMode(gin.DebugMode) + } else { + gin.SetMode(gin.ReleaseMode) + } + log.SetOutput(gin.DefaultWriter) + + // Set the router as the default one provided by Gin + router = gin.Default() + + // Process the templates at the start so that they don't have to be loaded + // from the disk again. This makes serving HTML pages very fast. + templatesDir := fmt.Sprintf("%s/**/*", TemplatesDir) + log.Printf("load template files from %v", templatesDir) + router.LoadHTMLGlob(templatesDir) + + // Use Middleware logger + router.Use(gin.Logger()) + + // Initialize the routes + setup.InitializeRoutes(router) + + // Clean up SetSessionStatus + setup.CleanupSessions() + + // Check if at least one user configured + setup.CheckUserList() + + // Check if data for TradeYourSportsCards is available + setup.CheckTradeYourSportsCardsData() + + //properties.SetVersion(Version) + + // Start serving the application + server := fmt.Sprintf("127.0.0.1:%d", Port) + router.Run(server) + }, +} + +// Execute parses the commandline and calls Execute on rootCmd. +func Execute(version string) { + rootCmd.Version = version + properties.SetVersion(version) + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output") + rootCmd.PersistentFlags().BoolVarP(&Debug, "debug", "d", false, "debug modus") + rootCmd.PersistentFlags().IntVarP(&Port, "port", "p", 8500, "port number") + rootCmd.PersistentFlags().StringVarP(&TemplatesDir, "templates", "t", "templates", "path for template files") +} diff --git a/go/cmd/server.go b/go/cmd/server.go new file mode 100644 index 0000000..3b7ebd0 --- /dev/null +++ b/go/cmd/server.go @@ -0,0 +1,32 @@ +package main + +import ( + "github.com/joho/godotenv" + application "gitlab.com/tpeetz-kontor/kontor-go/pkg/infrastructure/app" + "gitlab.com/tpeetz-kontor/kontor-go/pkg/infrastructure/kernel" +) + +// init is invoked before main() +func init() { + // loads values from .env into the system + if err := godotenv.Load(); err != nil { + panic("No .env file found") + } +} + +func main() { + // Create our application + app := kernel.Boot() + + // Build our services + //ping.BuildPingService(app) + + // Run our Application in a coroutine + go func() { + app.Run() + }() + + // Wait for termination signals and shut down gracefully + application.WaitForShutdown(app) + +} diff --git a/go/comics.xml b/go/comics.xml new file mode 100644 index 0000000..730fdeb --- /dev/null +++ b/go/comics.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/go/docs/build.gradle b/go/docs/build.gradle new file mode 100644 index 0000000..9f27a44 --- /dev/null +++ b/go/docs/build.gradle @@ -0,0 +1,7 @@ +plugins { + alias(versionsLibs.plugins.asciidoctorConvention) +} + +wrapper { + gradleVersion = "7.5" +} diff --git a/go/docs/gradle.properties b/go/docs/gradle.properties new file mode 100644 index 0000000..aeb4e86 --- /dev/null +++ b/go/docs/gradle.properties @@ -0,0 +1,2 @@ +description='Documentation for application kontor-go' +version=1.0.0-SNAPSHOT diff --git a/go/docs/gradle/wrapper/gradle-wrapper.jar b/go/docs/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/go/docs/gradle/wrapper/gradle-wrapper.jar differ diff --git a/go/docs/gradle/wrapper/gradle-wrapper.properties b/go/docs/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8049c68 --- /dev/null +++ b/go/docs/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/go/docs/gradlew b/go/docs/gradlew new file mode 100755 index 0000000..a69d9cb --- /dev/null +++ b/go/docs/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/go/docs/gradlew.bat b/go/docs/gradlew.bat new file mode 100644 index 0000000..53a6b23 --- /dev/null +++ b/go/docs/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/go/docs/settings.gradle b/go/docs/settings.gradle new file mode 100644 index 0000000..b956775 --- /dev/null +++ b/go/docs/settings.gradle @@ -0,0 +1 @@ +rootProject.name='kontor-go' diff --git a/go/docs/src/docs/asciidoc/kontor-go.adoc b/go/docs/src/docs/asciidoc/kontor-go.adoc new file mode 100644 index 0000000..5334cfb --- /dev/null +++ b/go/docs/src/docs/asciidoc/kontor-go.adoc @@ -0,0 +1,31 @@ += Projektbeschreibung Kontor +:author: Thomas Peetz +:email: +:doctype: article +:toc: left +:sectnums: + +== Einführung + +=== Zweck + +== Anforderungen + +== Implementierung + +=== Datenmodell + +== Betrieb + +[bibliography] +== Referenzen + +- [[[1]]] Thomas Peetz, Betriebshandbuch IBTP + +[index] +== Index + +== Tabellenverzeichnis + +[glossary] +== Glossar diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..cc805de --- /dev/null +++ b/go/go.mod @@ -0,0 +1,28 @@ +module gitlab.com/tpeetz-kontor/kontor-go + +go 1.22.2 + +require github.com/joho/godotenv v1.5.1 + +require ( + github.com/felixge/httpsnoop v1.0.3 // indirect + go.uber.org/multierr v1.10.0 // indirect +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/google/uuid v1.6.0 + github.com/gorilla/handlers v1.5.2 + github.com/gorilla/mux v1.8.1 + github.com/jinzhu/gorm v1.9.16 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/satori/go.uuid v1.2.0 // indirect + go.uber.org/zap v1.27.0 + golang.org/x/text v0.19.0 // indirect + golang.org/x/tools v0.21.1-0.20240531212143-b6235391adb3 // indirect + gorm.io/driver/mysql v1.5.7 // indirect + gorm.io/gorm v1.25.12 // indirect +) diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..3d35401 --- /dev/null +++ b/go/go.sum @@ -0,0 +1,62 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= +github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.21.1-0.20240531212143-b6235391adb3 h1:SHq4Rl+B7WvyM4XODon1LXtP7gcG49+7Jubt1gWWswY= +golang.org/x/tools v0.21.1-0.20240531212143-b6235391adb3/go.mod h1:bqv7PJ/TtlrzgJKhOAGdDUkUltQapRik/UEHubLVBWo= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= +honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= diff --git a/go/kontor-ansible.yml b/go/kontor-ansible.yml new file mode 100644 index 0000000..2e19328 --- /dev/null +++ b/go/kontor-ansible.yml @@ -0,0 +1,58 @@ +--- +# file: kontor-ansible.yml +- hosts: localhost + remote_user: root + tasks: + - name: create group kontor + group: + name: kontor + state: present + + - name: create user kontor + user: + name: kontor + home: /home/kontor + shell: /bin/bash + state: present + # generate_ssh_key: yes + # ssh_key_bits: 4096 + # ssh_key_file: .ssh/id_rsa + + - name: create log directory + file: + path: /var/log/kontor + state: directory + owner: kontor + group: kontor + mode: 0775 + recurse: true + + - name: create run directory + file: + path: /var/run/kontor + state: directory + owner: kontor + group: kontor + mode: 0775 + recurse: true + + - name: create directory + file: + path: /home/kontor/production + state: directory + owner: kontor + group: kontor + mode: 0775 + recurse: false + + - name: copy binary + copy: + src: bin/kontor + dest: /home/kontor/production/kontor + mode: 0775 + + - name: copy templates + copy: + src: templates + dest: /home/kontor/production/kontor/templates + mode: 0775 diff --git a/go/pkg/admin/routes.go b/go/pkg/admin/routes.go new file mode 100644 index 0000000..a4845cf --- /dev/null +++ b/go/pkg/admin/routes.go @@ -0,0 +1,23 @@ +package admin + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + + "github.com/gin-gonic/gin" +) + +// GetRoutes sets the routes for the administrative data urls. +func GetRoutes(router *gin.Engine) { + adminRoutes := router.Group("/admin") + { + adminRoutes.GET("/", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), showAdminIndex) + adminRoutes.GET("/user", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), showUserIndex) + adminRoutes.POST("/user", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), showUserIndex) + adminRoutes.GET("/user/view/:userid", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), showUserDetails) + adminRoutes.POST("/user/view", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), validateUserCreation) + adminRoutes.GET("/user/create", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), showUserCreation) + adminRoutes.POST("/user/create", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), validateUserCreation) + adminRoutes.GET("/data", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), showDataUpload) + adminRoutes.POST("/data", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), validateDataUpload) + } +} diff --git a/go/pkg/admin/user.go b/go/pkg/admin/user.go new file mode 100644 index 0000000..8d94a06 --- /dev/null +++ b/go/pkg/admin/user.go @@ -0,0 +1,18 @@ +package admin + +import ( + "gopkg.in/mgo.v2/bson" +) + +// User defines the data model for application users with id,email, user name, +// first and family name, password and admin status. +type User struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Email string `json:"email" bson:"email,omitempty"` + Username string `json:"username" bson:"username,omitempty"` + Firstname string `json:"firstname" bson:"firstname,omitempty"` + Lastname string `json:"lastname" bson:"lastname,omitempty"` + Password string `json:"password" bson:"password,omitempty"` + IsAdmin bool `json:"is_admin" bson:"is_admin,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/admin/user_dao.go b/go/pkg/admin/user_dao.go new file mode 100644 index 0000000..3f77fbd --- /dev/null +++ b/go/pkg/admin/user_dao.go @@ -0,0 +1,123 @@ +package admin + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "log" + + "github.com/gin-gonic/gin" + + "golang.org/x/crypto/bcrypt" + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// UserDAO extends the type BaseDAO. +type UserDAO struct { + Db dao.BaseDAO +} + +const ( + // USERCOLLECTION defines the collection name for storing application user data. + USERCOLLECTION = "user" + // USERMODEL defines the name of the user data model. + USERMODEL = "kontor.admin.user" +) + +// HashPassword returns the encrypted password from password string. +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + return string(bytes), err +} + +// CheckPasswordHash returns if password correlates with pasword hash. +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +// FindAll retrieves the list of users from the database. +func (m *UserDAO) FindAll() ([]User, error) { + m.Db.Connect() + var users []User + err := m.Db.MongoDb.C(USERCOLLECTION).Find(bson.M{"model": USERMODEL}).All(&users) + return users, err +} + +// FindByID returns a user with given id or returns the error. +func (m *UserDAO) FindByID(id string) (User, error) { + m.Db.Connect() + var user User + err := m.Db.MongoDb.C(USERCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&user) + return user, err +} + +// FindByUsername returns a user with given name or returns the error. +func (m *UserDAO) FindByUsername(username string) (User, error) { + m.Db.Connect() + var user User + err := m.Db.MongoDb.C(USERCOLLECTION).Find(bson.M{"username": username, "model": USERMODEL}).One(&user) + return user, err +} + +// Insert a user into database. +func (m *UserDAO) Insert(user User) error { + m.Db.Connect() + user.Model = USERMODEL + err := m.Db.MongoDb.C(USERCOLLECTION).Insert(&user) + return err +} + +// Upsert a user into database. +func (m *UserDAO) Upsert(user User) (*mgo.ChangeInfo, error) { + m.Db.Connect() + user.Model = USERMODEL + info, err := m.Db.MongoDb.C(USERCOLLECTION).Upsert(bson.M{"username": user.Username}, &user) + return info, err +} + +// Update an existing user. +func (m *UserDAO) Update(user User) error { + m.Db.Connect() + err := m.Db.MongoDb.C(USERCOLLECTION).UpdateId(user.ID, &user) + return err +} + +// Delete an existing user. +func (m *UserDAO) Delete(user User) error { + m.Db.Connect() + err := m.Db.MongoDb.C(USERCOLLECTION).Remove(&user) + return err +} + +// IsUserValid checks if the username and password combination is valid +func (m *UserDAO) IsUserValid(username, password string) bool { + if gin.IsDebugging() { + log.Printf("UserDAO.IsUserValid(%s)", username) + } + user, err := m.FindByUsername(username) + if gin.IsDebugging() { + log.Printf("UserDAO.IsUserValid: %v, %v", user, err) + } + if &user == nil || err != nil { + return false + } + return CheckPasswordHash(password, user.Password) +} + +// IsUserAdmin checks if user identified by name has admin rights. +func (m *UserDAO) IsUserAdmin(username string) bool { + user, err := m.FindByUsername(username) + if &user == nil || err != nil { + return false + } + return user.IsAdmin +} + +// IsUsernameAvailable checks if the supplied username is available. +func (m *UserDAO) IsUsernameAvailable(username string) bool { + user, err := m.FindByUsername(username) + if &user == nil || err != nil { + return true + } + return false +} diff --git a/go/pkg/admin/user_test.go b/go/pkg/admin/user_test.go new file mode 100644 index 0000000..0668ae2 --- /dev/null +++ b/go/pkg/admin/user_test.go @@ -0,0 +1,105 @@ +package admin + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var userModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Email", "string"}, + {"Username", "string"}, + {"Firstname", "string"}, + {"Lastname", "string"}, + {"Password", "string"}, + {"IsAdmin", "bool"}, + {"Model", "string"}, +} + +func TestUserModel(t *testing.T) { + m := User{} + if reflect.TypeOf(m).NumField() != len(userModelTestTable) { + t.Fail() + } + for index, testData := range userModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListUsers(t *testing.T) { + var userDao = UserDAO{Db: dao.TestDb} + users, err := userDao.FindAll() + if err != nil { + t.Fail() + } + if users != nil { + t.Fail() + } +} + +func TestInsertUser(t *testing.T) { + var ( + userDao = UserDAO{Db: dao.TestDb} + user = User{} + users []User + ) + user.ID = bson.NewObjectId() + user.Username = "test" + err := userDao.Insert(user) + if err != nil { + t.Fail() + } + users, err = userDao.FindAll() + if err != nil { + t.Fail() + } + if len(users) != 1 { + t.Fail() + } +} + +func TestUpsertUser(t *testing.T) { + var ( + userDao = UserDAO{Db: dao.TestDb} + user = User{} + ) + user.ID = bson.NewObjectId() + user.Username = "test2" + userDao.Upsert(user) + users, err := userDao.FindAll() + if err != nil { + t.Fail() + } + if len(users) != 2 { + t.Fail() + } +} + +func TestDeleteUser(t *testing.T) { + var ( + userDao = UserDAO{Db: dao.TestDb} + ) + users, err := userDao.FindAll() + if err != nil { + t.Fail() + } + for _, user := range users { + userDao.Delete(user) + } + users, err = userDao.FindAll() + if err != nil { + t.Fail() + } + if len(users) != 0 { + t.Fail() + } +} diff --git a/go/pkg/admin/views.go b/go/pkg/admin/views.go new file mode 100644 index 0000000..f1b62cc --- /dev/null +++ b/go/pkg/admin/views.go @@ -0,0 +1,242 @@ +package admin + +import ( + "io/ioutil" + "log" + "net/http" + "path/filepath" + "strconv" + + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/comics" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + + "github.com/gin-gonic/gin" + "github.com/smallfish/simpleyaml" +) + +const ( + // KontorUserAdministrationTitle defines the text of the page title + KontorUserAdministrationTitle = "Kontor User Administration" + // DataUploadTemplate defines the name of the template file for the data upload + DataUploadTemplate = "kontor/data-upload.html" +) + +// ShowLoginPage renders login page. +func ShowLoginPage(c *gin.Context) { + // Call the render function with the name of the template to render + util.Render(c, gin.H{"title": "Login"}, "login.html") +} + +// PerformLogin reads data from login form and validates input. +func PerformLogin(c *gin.Context) { + // Obtain the POSTed username and password values + username := c.PostForm("username") + password := c.PostForm("password") + + var userDao = UserDAO{Db: dao.KontorDb} + + // Check if the username/password combination is valid + if userDao.IsUserValid(username, password) { + sessionInterface, _ := c.Get("session") + sessionID := sessionInterface.(string) + user, _ := userDao.FindByUsername(username) + sessionDao := auth.SessionDAO{Db: dao.KontorDb} + session, _ := sessionDao.FindByID(sessionID) + session.Username = username + session.IsAdmin = user.IsAdmin + sessionDao.Update(session) + util.Render(c, gin.H{"title": "Successful Login", "InfoMessage": "Login successfull"}, "kontor/index.html") + } else { + // If the username/password combination is invalid, + // show the error message on the login page + c.HTML(http.StatusBadRequest, "login.html", gin.H{ + "ErrorTitle": "Login Failed", + "ErrorMessage": "Invalid credentials provided"}) + } +} + +// Logout invalidates session. +func Logout(c *gin.Context) { + sessionInterface, _ := c.Get("session") + sessionID := sessionInterface.(string) + c.SetCookie("session", sessionID, -1, "", "", false, true) + + // Redirect to the home page + c.Redirect(http.StatusTemporaryRedirect, "/") +} + +func showAdminIndex(c *gin.Context) { + // Call the render function with the name of the template to render + util.Render(c, gin.H{"title": "Kontor", "payload": nil}, "kontor/admin.html") +} + +func showUserIndex(c *gin.Context) { + var dao = UserDAO{Db: dao.KontorDb} + if users, err := dao.FindAll(); err == nil && users != nil { + util.Render(c, gin.H{"title": KontorUserAdministrationTitle, "payload": users}, "kontor/users.html") + } else { + util.Render(c, gin.H{"title": KontorUserAdministrationTitle, "payload": users, "ErrorMessage": err}, "kontor/users.html") + } +} + +func showUserDetails(c *gin.Context) { + userID := c.Param("userid") + var userDao = UserDAO{Db: dao.KontorDb} + if user, err := userDao.FindByID(userID); err == nil && &user != nil { + util.Render(c, gin.H{"title": KontorUserAdministrationTitle, "payload": user, "action": util.SaveAction}, "kontor/user-detail.html") + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} + +func showUserCreation(c *gin.Context) { + var user = User{} + util.Render(c, gin.H{"title": KontorUserAdministrationTitle, "payload": user, "action": util.AddAction}, "kontor/user-detail.html") +} + +func validateUserCreation(c *gin.Context) { + // Obtain the POSTed username and password values + username := c.PostForm("username") + firstname := c.PostForm("firstname") + lastname := c.PostForm("lastname") + password := c.PostForm("password") + adminFormVar := c.PostForm("admin") + action := c.PostForm("action") + userid := c.PostForm("userid") + isAdmin, _ := strconv.ParseBool(adminFormVar) + + var err error + var dao = UserDAO{Db: dao.KontorDb} + var user = User{} + + switch action { + case util.AddAction: + user.Username = username + user.Firstname = firstname + user.Lastname = lastname + user.IsAdmin = isAdmin + user.Password, _ = HashPassword(password) + _, err = dao.Upsert(user) + case util.SaveAction: + user, _ = dao.FindByID(userid) + user.Username = username + user.Firstname = firstname + user.Lastname = lastname + user.IsAdmin = isAdmin + user.Password, _ = HashPassword(password) + err = dao.Update(user) + case util.DeleteAction: + user, _ = dao.FindByID(userid) + err = dao.Delete(user) + } + if err == nil { + c.Redirect(http.StatusTemporaryRedirect, "/admin/user") + } else { + c.HTML(http.StatusBadRequest, "kontor/create-user.html", gin.H{ + "ErrorTitle": "User Creation Failed", + "ErrorMessage": err.Error()}) + } +} + +func showDataUpload(c *gin.Context) { + // Call the render function with the name of the template to render + //util.Render(c, gin.H{"title": "Kontor", "payload": nil}, "kontor/admin.html") + util.Render(c, gin.H{"title": "Kontor Data Upload", "payload": nil}, DataUploadTemplate) +} + +func validateDataUpload(c *gin.Context) { + // Call the render function with the name of the template to render + //util.Render(c, gin.H{"title": "Kontor", "payload": nil}, "kontor/admin.html") + datafile, err := c.FormFile("datafile") + if err != nil { + c.HTML(http.StatusBadRequest, DataUploadTemplate, gin.H{ + "ErrorTitle": "Data Upload Failed", + "ErrorMessage": err.Error()}) + return + } + log.Printf("Data File: %v", datafile.Filename) + filename := filepath.Base(datafile.Filename) + if err := c.SaveUploadedFile(datafile, filename); err != nil { + c.HTML(http.StatusBadRequest, DataUploadTemplate, gin.H{ + "ErrorTitle": "Data Upload Failed", + "ErrorMessage": err.Error()}) + return + } + source, err := ioutil.ReadFile(filename) + if err != nil { + panic(err) + } + yaml, err := simpleyaml.NewYaml(source) + if err != nil { + panic(err) + } + if yaml.IsArray() { + size, err := yaml.GetArraySize() + if err != nil { + panic(err) + } + log.Printf("Found %d entries.\n", size) + var publisherDao comics.PublisherDAO + publisherDao.Db = dao.KontorDb + var artistDao comics.ArtistDAO + artistDao.Db = dao.KontorDb + var comicDao comics.ComicDAO + comicDao.Db = dao.KontorDb + publisherMap := make(map[int]string) + for index := 0; index < size; index++ { + entry := yaml.GetIndex(index) + if entry.IsMap() { + model, err := entry.Get("model").String() + if err != nil { + panic(err) + } + pk, _ := entry.Get("pk").Int() + switch model { + case "comics.publisher": + name, err := entry.Get("fields").Get("name").String() + if err != nil { + panic(err) + } + log.Printf(" %v %v %v\n", pk, model, name) + publisherMap[pk] = name + publisher := comics.Publisher{} + publisher.Name = name + info, _ := publisherDao.Upsert(publisher) + log.Printf("Publisher records changed: %d", info.Updated) + case "comics.artist": + name, err := entry.Get("fields").Get("name").String() + if err != nil { + panic(err) + } + log.Printf(" %v %v %v\n", pk, model, name) + artist := comics.Artist{} + artist.Name = name + info, _ := artistDao.Upsert(artist) + log.Printf("Artist records changed: %d", info.Updated) + case "comics.comic": + title, err := entry.Get("fields").Get("title").String() + if err != nil { + panic(err) + } + publisherID, err := entry.Get("fields").Get("publisher").Int() + publisher, err := publisherDao.FindByName(publisherMap[publisherID]) + completed, err := entry.Get("fields").Get("completed").Bool() + if err != nil { + log.Printf("Error occured: %v", err) + } + log.Printf(" %v %v %v %v\n", pk, model, title, completed) + comic := comics.Comic{} + comic.Title = title + comic.Completed = completed + comic.Publisher = publisher.ID + info, _ := comicDao.Upsert(comic) + log.Printf("Comic records changed: %d", info.Updated) + } + //fmt.Printf("Entry %d: %v\n", index, entry) + } + } + } + util.Render(c, gin.H{"title": "Kontor Data Upload", "payload": nil}, DataUploadTemplate) +} diff --git a/go/pkg/application/registry/comic/registry.go b/go/pkg/application/registry/comic/registry.go new file mode 100644 index 0000000..7fb2f95 --- /dev/null +++ b/go/pkg/application/registry/comic/registry.go @@ -0,0 +1,19 @@ +package comic + +import ( + "net/http" + + "gitlab.com/tpeetz-kontor/kontor-go/cmd/pkg/context/comic/routing" + "gitlab.com/tpeetz-kontor/kontor-go/cmd/pkg/infrastructure/app" +) + +func BuildComicService(app *app.Application) { + // Create our Handler + handler := routing.NewHandler(app) + + // Create a sub router for this service + router := app.Router.Methods(http.MethodGet).Subrouter() + + // Register our service routes + router.HandleFunc("/comics/comic", handler.ComicList).Name("comics:comicList") +} diff --git a/go/pkg/auth/middleware.go b/go/pkg/auth/middleware.go new file mode 100644 index 0000000..a26a93d --- /dev/null +++ b/go/pkg/auth/middleware.go @@ -0,0 +1,87 @@ +package auth + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/properties" + "log" + "net/http" + + "github.com/gin-gonic/gin" + "gopkg.in/mgo.v2/bson" +) + +var sessionDao = SessionDAO{Db: dao.KontorDb} + +// EnsureLoggedIn ensures that a request will be aborted with an error +// if the user is not logged in +func EnsureLoggedIn() gin.HandlerFunc { + return func(c *gin.Context) { + // If there's an error or if the token is empty + // the user is not logged in + sessionInterface, _ := c.Get("session") + sessionID := sessionInterface.(string) + if session, err := sessionDao.GetSession(sessionID); err != nil || session.Username == "" { + c.Redirect(http.StatusTemporaryRedirect, "/") + //c.AbortWithStatus(http.StatusUnauthorized) + } + } +} + +// EnsureAdminStatus ensures that a request will be aborted with an error +// if the user is not logged in +func EnsureAdminStatus() gin.HandlerFunc { + return func(c *gin.Context) { + // If there's an error or if the token is empty + // the user is not logged in + sessionInterface, _ := c.Get("session") + sessionID := sessionInterface.(string) + if session, err := sessionDao.GetSession(sessionID); err != nil || !session.IsAdmin { + c.Redirect(http.StatusTemporaryRedirect, "/") + //c.AbortWithStatus(http.StatusUnauthorized) + } + } +} + +// EnsureNotLoggedIn ensures that a request will be aborted with an error +// if the user is already logged in +func EnsureNotLoggedIn() gin.HandlerFunc { + return func(c *gin.Context) { + // If there's no error or if the token is not empty + // the user is already logged in + sessionInterface, _ := c.Get("session") + sessionID := sessionInterface.(string) + if session, err := sessionDao.GetSession(sessionID); err != nil || session.Username != "" { + c.Redirect(http.StatusTemporaryRedirect, "/") + //c.AbortWithStatus(http.StatusUnauthorized) + } + } +} + +// SetSessionStatus reads sessionId from cookie if available or create new session object +// and sets cookie accordingly. +func SetSessionStatus() gin.HandlerFunc { + return func(c *gin.Context) { + if sessionID, err := c.Cookie("session"); err == nil || sessionID != "" { + c.Set("session", sessionID) + } else { + session, _ := sessionDao.GetSession(bson.NewObjectId().Hex()) + sessionID := session.ID.Hex() + c.Set("session", sessionID) + c.SetCookie("session", sessionID, 3600, "", "", false, true) + } + } +} + +// SetSessionData populates session information with username, admin status of user and +// application version. +func SetSessionData(c *gin.Context, data gin.H) { + sessionInterface, _ := c.Get("session") + sessionID := sessionInterface.(string) + session, _ := sessionDao.GetSession(sessionID) + if gin.IsDebugging() { + log.Printf("setSessionData(%v): %v", sessionID, session) + } + data["is_logged_in"] = (session.Username != "") + data["is_admin"] = session.IsAdmin + data["version"] = properties.Version +} diff --git a/go/pkg/auth/session.go b/go/pkg/auth/session.go new file mode 100644 index 0000000..dccf9fb --- /dev/null +++ b/go/pkg/auth/session.go @@ -0,0 +1,11 @@ +package auth + +import "gopkg.in/mgo.v2/bson" + +// Session defines the data model for sessions with id,user name and admin status. +type Session struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Username string `json:"username" bson:"username,omitempty"` + IsAdmin bool `json:"is_admin" bson:"is_admin,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/auth/session_dao.go b/go/pkg/auth/session_dao.go new file mode 100644 index 0000000..70f7adc --- /dev/null +++ b/go/pkg/auth/session_dao.go @@ -0,0 +1,78 @@ +package auth + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// SessionDAO extends the type BaseDAO. +type SessionDAO struct { + Db dao.BaseDAO +} + +const ( + // SESSIONCOLLECTION defines the collection name for storing session data. + SESSIONCOLLECTION = "session" + // SESSIONMODEL defines the name of the session data model. + SESSIONMODEL = "kontor.admin.session" +) + +// FindAll retrieves the list of sessions from the database. +func (m *SessionDAO) FindAll() ([]Session, error) { + m.Db.Connect() + var sessions []Session + err := m.Db.MongoDb.C(SESSIONCOLLECTION).Find(bson.M{"model": SESSIONMODEL}).All(&sessions) + return sessions, err +} + +// FindByID returns a session with given id or returns the error. +func (m *SessionDAO) FindByID(id string) (Session, error) { + m.Db.Connect() + var session Session + err := m.Db.MongoDb.C(SESSIONCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&session) + return session, err +} + +// Insert a session into database. +func (m *SessionDAO) Insert(session Session) error { + m.Db.Connect() + session.Model = SESSIONMODEL + err := m.Db.MongoDb.C(SESSIONCOLLECTION).Insert(&session) + //log.PrintDebug("Insert: %v, %v\n", session, err) + return err +} + +// Upsert a session into database. +func (m *SessionDAO) Upsert(session Session) (*mgo.ChangeInfo, error) { + m.Db.Connect() + session.Model = SESSIONMODEL + info, err := m.Db.MongoDb.C(SESSIONCOLLECTION).Upsert(bson.M{"_id": session.ID}, &session) + return info, err +} + +// Update an existing session. +func (m *SessionDAO) Update(session Session) error { + m.Db.Connect() + err := m.Db.MongoDb.C(SESSIONCOLLECTION).UpdateId(session.ID, &session) + return err +} + +// Delete an existing session. +func (m *SessionDAO) Delete(session Session) error { + m.Db.Connect() + err := m.Db.MongoDb.C(SESSIONCOLLECTION).Remove(&session) + return err +} + +// GetSession get a session by given id or create a new one, if nothing was found. +func (m *SessionDAO) GetSession(id string) (*Session, error) { + m.Db.Connect() + session, err := m.FindByID(id) + if err != nil { + session = Session{ID: bson.ObjectIdHex(id), Username: "", IsAdmin: false} + m.Insert(session) + } + return &session, nil +} diff --git a/go/pkg/auth/session_test.go b/go/pkg/auth/session_test.go new file mode 100644 index 0000000..a6769b8 --- /dev/null +++ b/go/pkg/auth/session_test.go @@ -0,0 +1,103 @@ +package auth + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var sessionModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Username", "string"}, + {"IsAdmin", "bool"}, + {"Model", "string"}, +} + +func TestSessionModel(t *testing.T) { + m := Session{} + if reflect.TypeOf(m).NumField() != len(sessionModelTestTable) { + t.Fail() + } + for index, testData := range sessionModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListSessions(t *testing.T) { + var ( + sessionDao = SessionDAO{Db: dao.TestDb} + ) + sessions, err := sessionDao.FindAll() + if err != nil { + t.Fail() + } + if sessions != nil { + t.Fail() + } +} + +func TestInsertSession(t *testing.T) { + var ( + sessionDao = SessionDAO{Db: dao.TestDb} + session = Session{} + sessions []Session + ) + session.ID = bson.NewObjectId() + session.Username = "test" + err := sessionDao.Insert(session) + if err != nil { + t.Fail() + } + sessions, err = sessionDao.FindAll() + if err != nil { + t.Fail() + } + if len(sessions) != 1 { + t.Fail() + } +} + +func TestUpsertSession(t *testing.T) { + var ( + sessionDao = SessionDAO{Db: dao.TestDb} + session = Session{} + ) + session.ID = bson.NewObjectId() + session.Username = "test2" + sessionDao.Upsert(session) + sessions, err := sessionDao.FindAll() + if err != nil { + t.Fail() + } + if len(sessions) != 2 { + t.Fail() + } +} + +func TestDeleteSession(t *testing.T) { + var ( + sessionDao = SessionDAO{Db: dao.TestDb} + ) + sessions, err := sessionDao.FindAll() + if err != nil { + t.Fail() + } + for _, session := range sessions { + sessionDao.Delete(session) + } + sessions, err = sessionDao.FindAll() + if err != nil { + t.Fail() + } + if len(sessions) != 0 { + t.Fail() + } +} diff --git a/go/pkg/comics/artist.go b/go/pkg/comics/artist.go new file mode 100644 index 0000000..8afd80a --- /dev/null +++ b/go/pkg/comics/artist.go @@ -0,0 +1,12 @@ +package comics + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Artist defines the data model for comic artists with id and name. +type Artist struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Model string `json:"model" bson:"model"` +} diff --git a/go/pkg/comics/artist_dao.go b/go/pkg/comics/artist_dao.go new file mode 100644 index 0000000..b177c2b --- /dev/null +++ b/go/pkg/comics/artist_dao.go @@ -0,0 +1,78 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "log" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// ArtistDAO extends the type BaseDAO. +type ArtistDAO struct { + Db dao.BaseDAO +} + +const ( + // ARTISTCOLLECTION defines the collection name for storing comic artists. + ARTISTCOLLECTION = "artist" + // ARTISTMODEL defines the name of the artist data model. + ARTISTMODEL = "kontor.comics.artist" +) + +// FindAll retrieves the list of artists from the database. +func (m *ArtistDAO) FindAll() ([]Artist, error) { + m.Db.Connect() + var artists []Artist + err := m.Db.MongoDb.C(ARTISTCOLLECTION).Find(bson.M{"model": ARTISTMODEL}).All(&artists) + return artists, err +} + +// FindByID returns an artists with given id or returns the error. +func (m *ArtistDAO) FindByID(id string) (Artist, error) { + m.Db.Connect() + var artist Artist + err := m.Db.MongoDb.C(ARTISTCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&artist) + return artist, err +} + +// FindByName returns an artists with given name or returns the error. +func (m *ArtistDAO) FindByName(name string) (Artist, error) { + m.Db.Connect() + var artist Artist + err := m.Db.MongoDb.C(ARTISTCOLLECTION).Find(bson.M{"name": name, "model": ARTISTMODEL}).One(&artist) + return artist, err +} + +// Insert an artist into database. +func (m *ArtistDAO) Insert(artist Artist) error { + m.Db.Connect() + artist.Model = ARTISTMODEL + err := m.Db.MongoDb.C(ARTISTCOLLECTION).Insert(&artist) + return err +} + +// Upsert an artist into database. +func (m *ArtistDAO) Upsert(artist Artist) (*mgo.ChangeInfo, error) { + m.Db.Connect() + artist.Model = ARTISTMODEL + info, err := m.Db.MongoDb.C(ARTISTCOLLECTION).Upsert(bson.M{"name": artist.Name}, &artist) + return info, err +} + +// Delete an existing artist. +func (m *ArtistDAO) Delete(artist Artist) error { + m.Db.Connect() + err := m.Db.MongoDb.C(ARTISTCOLLECTION).Remove(&artist) + if err != nil { + log.Printf("ArtistDao.Delete: %v", err) + } + return err +} + +// Update an existing artist. +func (m *ArtistDAO) Update(artist Artist) error { + m.Db.Connect() + err := m.Db.MongoDb.C(ARTISTCOLLECTION).UpdateId(artist.ID, &artist) + return err +} diff --git a/go/pkg/comics/artist_test.go b/go/pkg/comics/artist_test.go new file mode 100644 index 0000000..96e23ca --- /dev/null +++ b/go/pkg/comics/artist_test.go @@ -0,0 +1,96 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var artistModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Model", "string"}, +} + +func TestArtistModel(t *testing.T) { + m := Artist{} + if reflect.TypeOf(m).NumField() != len(artistModelTestTable) { + t.Fail() + } + for index, testData := range artistModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListArtists(t *testing.T) { + var ( + artistDao = ArtistDAO{Db: dao.TestDb} + ) + artists, err := artistDao.FindAll() + if err != nil { + t.Fail() + } + if artists != nil { + t.Fail() + } +} + +func TestInsertArtist(t *testing.T) { + var ( + artistDao = ArtistDAO{Db: dao.TestDb} + artist = Artist{} + artists []Artist + ) + artist.ID = bson.NewObjectId() + artist.Name = "Turner, Michael" + err := artistDao.Insert(artist) + if err != nil { + t.Fail() + } + artists, _ = artistDao.FindAll() + if len(artists) != 1 { + t.Fail() + } +} + +func TestUpsertArtist(t *testing.T) { + var ( + artistDao = ArtistDAO{Db: dao.TestDb} + artist = Artist{} + ) + artist.ID = bson.NewObjectId() + artist.Name = "Marz, Ron" + _, err := artistDao.Upsert(artist) + if err != nil { + t.Fail() + } + artists, _ := artistDao.FindAll() + if len(artists) != 2 { + t.Fail() + } +} + +func TestDeleteArtist(t *testing.T) { + var ( + artistDao = ArtistDAO{Db: dao.TestDb} + ) + artists, err := artistDao.FindAll() + if err != nil { + t.Fail() + } + for _, artist := range artists { + artistDao.Delete(artist) + } + artists, _ = artistDao.FindAll() + if len(artists) != 0 { + t.Fail() + } +} diff --git a/go/pkg/comics/artist_views.go b/go/pkg/comics/artist_views.go new file mode 100644 index 0000000..c685f1b --- /dev/null +++ b/go/pkg/comics/artist_views.go @@ -0,0 +1,23 @@ +package comics + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" +) + +const ( + // ArtistPublisherTemplate defines name of template file for comics publishers + ArtistPublisherTemplate = "comics/publishers.html" +) + +func showArtistList(c *gin.Context) { + var dao = ArtistDAO{Db: dao.KontorDb} + if artists, err := dao.FindAll(); err == nil { + util.Render(c, gin.H{"title": "Comic Artists", "payload": artists}, "artists.html") + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} diff --git a/go/pkg/comics/comic.go b/go/pkg/comics/comic.go new file mode 100644 index 0000000..7332d65 --- /dev/null +++ b/go/pkg/comics/comic.go @@ -0,0 +1,15 @@ +package comics + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Comic defines the data model for comic issues with id, title, publisher, order and completion status. +type Comic struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` + Publisher bson.ObjectId `json:"publisher" bson:"publisher,omitempty"` + CurrentOrder bool `json:"current_order" bson:"current_order"` + Completed bool `json:"completed" bson:"completed"` + Model string `json:"model" bson:"model"` +} diff --git a/go/pkg/comics/comic_dao.go b/go/pkg/comics/comic_dao.go new file mode 100644 index 0000000..c9f9b02 --- /dev/null +++ b/go/pkg/comics/comic_dao.go @@ -0,0 +1,75 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// ComicDAO extends the type BaseDAO. +type ComicDAO struct { + Db dao.BaseDAO +} + +const ( + // COMICCOLLECTION defines the collection name for storing comics. + COMICCOLLECTION = "comic" + // COMICMODEL defines the name of the comic data model. + COMICMODEL = "kontor.comics.comic" +) + +// FindAll retrieves the list of comisc from the database. +func (m *ComicDAO) FindAll() ([]Comic, error) { + m.Db.Connect() + var comics []Comic + err := m.Db.MongoDb.C(COMICCOLLECTION).Find(bson.M{"model": COMICMODEL}).All(&comics) + return comics, err +} + +// FindByID returns an comic with given id or returns the error. +func (m *ComicDAO) FindByID(id string) (Comic, error) { + m.Db.Connect() + var comic Comic + err := m.Db.MongoDb.C(COMICCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&comic) + return comic, err +} + +// FindByName returns an comic with given name or returns the error. +func (m *ComicDAO) FindByName(name string) (Comic, error) { + m.Db.Connect() + var comic Comic + err := m.Db.MongoDb.C(COMICCOLLECTION).Find(bson.M{"name": name, "model": COMICMODEL}).One(&comic) + return comic, err +} + +// Insert a comic into database. +func (m *ComicDAO) Insert(comic Comic) error { + m.Db.Connect() + comic.Model = COMICMODEL + err := m.Db.MongoDb.C(COMICCOLLECTION).Insert(&comic) + //util.PrintDebug("ComicDAO.Insert: %v", comic) + return err +} + +// Upsert a comic into database. +func (m *ComicDAO) Upsert(comic Comic) (*mgo.ChangeInfo, error) { + m.Db.Connect() + comic.Model = COMICMODEL + info, err := m.Db.MongoDb.C(COMICCOLLECTION).Upsert(bson.M{"title": comic.Title}, &comic) + return info, err +} + +// Delete an existing comic. +func (m *ComicDAO) Delete(comic Comic) error { + m.Db.Connect() + err := m.Db.MongoDb.C(COMICCOLLECTION).Remove(&comic) + return err +} + +// Update an existing movie +func (m *ComicDAO) Update(comic Comic) error { + m.Db.Connect() + err := m.Db.MongoDb.C(COMICCOLLECTION).UpdateId(comic.ID, &comic) + return err +} diff --git a/go/pkg/comics/comic_test.go b/go/pkg/comics/comic_test.go new file mode 100644 index 0000000..baea792 --- /dev/null +++ b/go/pkg/comics/comic_test.go @@ -0,0 +1,88 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var comicModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Title", "string"}, + {"Publisher", "string"}, + {"CurrentOrder", "bool"}, + {"Completed", "bool"}, + {"Model", "string"}, +} + +func TestComicModel(t *testing.T) { + m := Comic{} + if reflect.TypeOf(m).NumField() != len(comicModelTestTable) { + t.Fail() + } + for index, testData := range comicModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListComics(t *testing.T) { + var ( + comicDao = ComicDAO{Db: dao.TestDb} + ) + comics, err := comicDao.FindAll() + if err != nil { + t.Fail() + } + if len(comics) != 0 { + t.Fail() + } +} + +func TestInsertComic(t *testing.T) { + var ( + comicDao = ComicDAO{Db: dao.TestDb} + comic = Comic{} + comics []Comic + ) + comic.ID = bson.NewObjectId() + comic.Title = "Simpsons" + comicDao.Insert(comic) + comics, _ = comicDao.FindAll() + if len(comics) != 1 { + t.Fail() + } +} +func TestUpsertComic(t *testing.T) { + var ( + comicDao = ComicDAO{Db: dao.TestDb} + comic = Comic{} + ) + comic.ID = bson.NewObjectId() + comic.Title = "1602" + comicDao.Upsert(comic) + comics, _ := comicDao.FindAll() + if len(comics) != 2 { + t.Fail() + } +} +func TestDeleteComic(t *testing.T) { + var ( + comicDao = ComicDAO{Db: dao.TestDb} + ) + comics, _ := comicDao.FindAll() + for _, comic := range comics { + comicDao.Delete(comic) + } + comics, _ = comicDao.FindAll() + if len(comics) != 0 { + t.Fail() + } +} diff --git a/go/pkg/comics/publisher.go b/go/pkg/comics/publisher.go new file mode 100644 index 0000000..7bef875 --- /dev/null +++ b/go/pkg/comics/publisher.go @@ -0,0 +1,12 @@ +package comics + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Publisher defines the data model for comic publishers with id and name. +type Publisher struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Model string `json:"model" bson:"model"` +} diff --git a/go/pkg/comics/publisher_dao.go b/go/pkg/comics/publisher_dao.go new file mode 100644 index 0000000..f12cf22 --- /dev/null +++ b/go/pkg/comics/publisher_dao.go @@ -0,0 +1,75 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// PublisherDAO extends the type BaseDAO. +type PublisherDAO struct { + Db dao.BaseDAO +} + +const ( + // PUBLISHERCOLLECTION defines the collection name for storing publishers. + PUBLISHERCOLLECTION = "publisher" + // PUBLISHERMODEL defines the name of the publisher data model. + PUBLISHERMODEL = "kontor.comics.publisher" +) + +// FindAll retrieves the list of publishers from the database. +func (m *PublisherDAO) FindAll() ([]Publisher, error) { + m.Db.Connect() + var publishers []Publisher + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Find(bson.M{"model": PUBLISHERMODEL}).All(&publishers) + return publishers, err +} + +// FindByID returns an publisher with given id or returns the error. +func (m *PublisherDAO) FindByID(id string) (Publisher, error) { + m.Db.Connect() + var publisher Publisher + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&publisher) + return publisher, err +} + +// FindByName returns an comic with given name or returns the error. +func (m *PublisherDAO) FindByName(name string) (Publisher, error) { + m.Db.Connect() + var publisher Publisher + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Find(bson.M{"name": name, "model": PUBLISHERMODEL}).One(&publisher) + return publisher, err +} + +// Insert a publisher into database. +func (m *PublisherDAO) Insert(publisher Publisher) error { + m.Db.Connect() + publisher.Model = PUBLISHERMODEL + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Insert(&publisher) + //util.PrintDebug("PublisherDAO.Insert: %v", publisher) + return err +} + +// Upsert a publisher into database. +func (m *PublisherDAO) Upsert(publisher Publisher) (*mgo.ChangeInfo, error) { + m.Db.Connect() + publisher.Model = PUBLISHERMODEL + info, err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Upsert(bson.M{"name": publisher.Name}, &publisher) + return info, err +} + +// Delete an existing publisher. +func (m *PublisherDAO) Delete(publisher Publisher) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Remove(&publisher) + return err +} + +// Update an existing publisher. +func (m *PublisherDAO) Update(publisher Publisher) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).UpdateId(publisher.ID, &publisher) + return err +} diff --git a/go/pkg/comics/publisher_test.go b/go/pkg/comics/publisher_test.go new file mode 100644 index 0000000..289c2cf --- /dev/null +++ b/go/pkg/comics/publisher_test.go @@ -0,0 +1,87 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var publisherModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Model", "string"}, +} + +func TestPublisherModel(t *testing.T) { + m := Publisher{} + if reflect.TypeOf(m).NumField() != len(publisherModelTestTable) { + t.Fail() + } + for index, testData := range publisherModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListPublishers(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + ) + publishers, err := publisherDao.FindAll() + if err != nil { + t.Fail() + } + if len(publishers) != 0 { + t.Fail() + } +} + +func TestInsertPublisher(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + publisher = Publisher{} + publishers []Publisher + ) + publisher.ID = bson.NewObjectId() + publisher.Name = "CrossGen" + publisherDao.Insert(publisher) + publishers, _ = publisherDao.FindAll() + if len(publishers) != 1 { + t.Fail() + } +} + +func TestUpsertPublisher(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + publisher = Publisher{} + ) + publisher.ID = bson.NewObjectId() + publisher.Name = "Marvel" + publisherDao.Upsert(publisher) + publishers, _ := publisherDao.FindAll() + if len(publishers) != 2 { + t.Fail() + } +} + +func TestDeletePublisher(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + ) + publishers, _ := publisherDao.FindAll() + for _, publisher := range publishers { + publisherDao.Delete(publisher) + } + publishers, _ = publisherDao.FindAll() + if len(publishers) != 0 { + t.Fail() + } +} diff --git a/go/pkg/comics/publisher_views.go b/go/pkg/comics/publisher_views.go new file mode 100644 index 0000000..e6acfc9 --- /dev/null +++ b/go/pkg/comics/publisher_views.go @@ -0,0 +1,70 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + "net/http" + + "github.com/gin-gonic/gin" +) + +const ( + // ComicsPublisherTemplate defines name of template file for comics publishers + ComicsPublisherTemplate = "comics/publishers.html" + // ComicsPublisherDetailsTemplate defines name of template file for comics publishers + ComicsPublisherDetailsTemplate = "comics/publisher.html" +) + +func showPublisherList(c *gin.Context) { + var dao = PublisherDAO{Db: dao.KontorDb} + if publishers, err := dao.FindAll(); err == nil { + util.Render(c, gin.H{"title": "Comics Publisher List", "payload": publishers}, ComicsPublisherTemplate) + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} + +func showPublisherDetails(c *gin.Context) { + var dao = PublisherDAO{Db: dao.KontorDb} + publisherid := c.Param("publisher_id") + if publisher, err := dao.FindByID(publisherid); err == nil { + util.Render(c, gin.H{"title": "Comics Publisher", "payload": publisher, "action": util.SaveAction}, ComicsPublisherDetailsTemplate) + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} + +func showPublisherCreation(c *gin.Context) { + var publisher = Publisher{} + util.Render(c, gin.H{"title": "Comics Publisher Creation", "payload": publisher, "action": util.AddAction}, ComicsPublisherTemplate) +} + +func validatePublisherDetails(c *gin.Context) { + name := c.PostForm("name") + action := c.PostForm("action") + publisherid := c.PostForm("publisherid") + + var err error + var dao = PublisherDAO{Db: dao.KontorDb} + var publisher = Publisher{} + + switch action { + case util.AddAction: + publisher.Name = name + _, err = dao.Upsert(publisher) + case util.SaveAction: + publisher, _ = dao.FindByID(publisherid) + publisher.Name = name + err = dao.Update(publisher) + case util.DeleteAction: + publisher, _ = dao.FindByID(publisherid) + err = dao.Delete(publisher) + } + if err == nil { + c.Redirect(http.StatusTemporaryRedirect, "/comics/publisher") + } else { + c.HTML(http.StatusBadRequest, "comics/publisher.html", gin.H{ + "ErrorTitle": "Publisher Creation Failed", + "ErrorMessage": err.Error()}) + } +} diff --git a/go/pkg/comics/routes.go b/go/pkg/comics/routes.go new file mode 100644 index 0000000..784704e --- /dev/null +++ b/go/pkg/comics/routes.go @@ -0,0 +1,28 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + + "github.com/gin-gonic/gin" +) + +// GetRoutes returns all routes for comic related data. +func GetRoutes(router *gin.Engine) { + comicRoutes := router.Group("/comics") + { + comicRoutes.GET("/", auth.EnsureLoggedIn(), showComicList) + comicRoutes.GET("/artist", auth.EnsureLoggedIn(), showArtistList) + comicRoutes.GET("/artist/view/:artist_id", auth.EnsureLoggedIn(), showArtistList) + comicRoutes.GET("/artist/create", auth.EnsureLoggedIn(), showArtistList) + comicRoutes.POST("/artist/create", auth.EnsureLoggedIn(), showArtistList) + comicRoutes.GET("/comic", auth.EnsureLoggedIn(), showComicList) + comicRoutes.GET("/comic/view/:comic_id", auth.EnsureLoggedIn(), showComic) + comicRoutes.GET("/comic/create", auth.EnsureLoggedIn(), showComicList) + comicRoutes.POST("/comic/create", auth.EnsureLoggedIn(), showComicList) + comicRoutes.GET("/publisher", auth.EnsureLoggedIn(), showPublisherList) + comicRoutes.POST("/publisher", auth.EnsureLoggedIn(), showPublisherList) + comicRoutes.GET("/publisher/view/:publisher_id", auth.EnsureLoggedIn(), showPublisherDetails) + comicRoutes.POST("/publisher/validate", auth.EnsureLoggedIn(), validatePublisherDetails) + comicRoutes.GET("/publisher/create", auth.EnsureLoggedIn(), showPublisherCreation) + } +} diff --git a/go/pkg/comics/views.go b/go/pkg/comics/views.go new file mode 100644 index 0000000..f1e9592 --- /dev/null +++ b/go/pkg/comics/views.go @@ -0,0 +1,28 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + "net/http" + + "github.com/gin-gonic/gin" +) + +func showComicList(c *gin.Context) { + var dao = ComicDAO{Db: dao.KontorDb} + if comics, err := dao.FindAll(); err == nil { + util.Render(c, gin.H{"title": "Comics", "payload": comics}, "comics.html") + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} + +func showComic(c *gin.Context) { + var dao = ComicDAO{Db: dao.KontorDb} + comicID := c.Param("comic_id") + if comic, err := dao.FindByID(comicID); err == nil { + util.Render(c, gin.H{"title": "Comic Details", "payload": comic, "action": util.SaveAction}, "comic.html") + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} diff --git a/go/pkg/context/comic/responses/comic.go b/go/pkg/context/comic/responses/comic.go new file mode 100644 index 0000000..a8b6c06 --- /dev/null +++ b/go/pkg/context/comic/responses/comic.go @@ -0,0 +1,15 @@ +package responses + +// Response is the Ping Response +type Response struct { + Message string `json:"message"` +} + +type ComicList struct { + Comics []Comic `json:"comics"` +} + +type Comic struct { + ID string `json:"id"` + Title string `json:"title"` +} diff --git a/go/pkg/context/comic/routing/endpoints.go b/go/pkg/context/comic/routing/endpoints.go new file mode 100644 index 0000000..abe4187 --- /dev/null +++ b/go/pkg/context/comic/routing/endpoints.go @@ -0,0 +1,33 @@ +package routing + +import ( + "net/http" + + "gitlab.com/tpeetz-kontor/kontor-go/cmd/pkg/context/comic/responses" + "gitlab.com/tpeetz-kontor/kontor-go/cmd/pkg/infrastructure/app" + responseFactory "gitlab.com/tpeetz-kontor/kontor-go/cmd/pkg/infrastructure/response" +) + +// Handler is the http.Handler for this request +type Handler struct { + app *app.Application +} + +// NewHandler will create a new Handler to handle this request +func NewHandler(app *app.Application) *Handler { + return &Handler{app} +} + +// Handle will handle the incoming request +func (handler *Handler) ComicList(response http.ResponseWriter, request *http.Request) { + handler.app.Logger.Info("Ping Handler Dispatched.") + + responseFactory.Send( + response, + http.StatusOK, + &responses.ComicList{ + Comics: []responses.Comic{{ID: "123", Title: "Comic1"}, {ID: "123", Title: "Comic1"}}, + }, + handler.app.Config.HTTP.Content, + ) +} diff --git a/go/pkg/dao/database.go b/go/pkg/dao/database.go new file mode 100644 index 0000000..1a0f4c1 --- /dev/null +++ b/go/pkg/dao/database.go @@ -0,0 +1,31 @@ +package dao + +import ( + "log" + + mgo "gopkg.in/mgo.v2" +) + +// BaseDAO definess the connection parameters to a MongoDB instance. +type BaseDAO struct { + Server string + Database string + MongoDb *mgo.Database +} + +var ( + // KontorDb has the database connection to the productive MongoDB instance. + KontorDb = BaseDAO{Server: "localhost", Database: "kontor"} + // TestDb has the database connection to the test MongoDB instance. + TestDb = BaseDAO{Server: "localhost", Database: "kontor_test"} +) + +// Connect instantiates the database session. +func (m *BaseDAO) Connect() { + session, err := mgo.Dial(m.Server) + if err != nil { + //util.PrintDebug("Connect: %v", err) + log.Fatal(err) + } + m.MongoDb = session.DB(m.Database) +} diff --git a/go/pkg/dao/database_test.go b/go/pkg/dao/database_test.go new file mode 100644 index 0000000..52a204c --- /dev/null +++ b/go/pkg/dao/database_test.go @@ -0,0 +1,51 @@ +package dao + +import ( + "reflect" + "testing" +) + + +var baseDaoTestTable = []struct { + name string + typeName string +}{ + {"Server", "string"}, + {"Database", "string"}, + {"MongoDb", "ptr"}, +} + +func TestCheckBaseDao(t *testing.T) { + d := BaseDAO{} + for index, testData := range baseDaoTestTable { + givenType := reflect.TypeOf(d).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestConnectDb(t *testing.T) { + d := BaseDAO{} + d.Connect() + if d.MongoDb == nil { + t.Fail() + } +} + +func TestDatabasesConfig(t *testing.T) { + kontorDb := KontorDb + if kontorDb.Server != "localhost" { + t.Fail() + } + if kontorDb.Database != "kontor" { + t.Fail() + } + testDb := TestDb + if testDb.Server != "localhost" { + t.Fail() + } + if testDb.Database != "kontor_test" { + t.Fail() + } +} diff --git a/go/pkg/infrastructure/app/factory.go b/go/pkg/infrastructure/app/factory.go new file mode 100644 index 0000000..3bc8ce7 --- /dev/null +++ b/go/pkg/infrastructure/app/factory.go @@ -0,0 +1,50 @@ +package app + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gorilla/mux" + "gitlab.com/tpeetz-kontor/kontor-go/pkg/infrastructure/config" + "go.uber.org/zap" +) + +// Application is our general purpose Application struct +type Application struct { + Server *http.Server + Router *mux.Router + Logger *zap.Logger + Config *config.Config +} + +// Run will run the Application server +func (app *Application) Run() { + app.Logger.Info("App started...") + err := app.Server.ListenAndServe() + + if err != nil { + fmt.Println(err) + } +} + +// WaitForShutdown is a graceful way to handle server shutdown events +func WaitForShutdown(application *Application) { + // Create a channel to listen for OS signals + interruptChan := make(chan os.Signal, 1) + signal.Notify(interruptChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + // Block until we receive a signal to our channel + <-interruptChan + + application.Logger.Info("Received shutdown signal, gracefully terminating") + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + application.Server.Shutdown(ctx) + os.Exit(0) +} diff --git a/go/pkg/infrastructure/config/factory.go b/go/pkg/infrastructure/config/factory.go new file mode 100644 index 0000000..ec411f8 --- /dev/null +++ b/go/pkg/infrastructure/config/factory.go @@ -0,0 +1,48 @@ +package config + +import "os" + +// AppConfig is the Application configuration struct +type AppConfig struct { + Name string + Version string + Token string +} + +// HTTPConfig is the Application HTTP configuration +type HTTPConfig struct { + Content string + Problem string + Port string +} + +// Config is the Configuration struct +type Config struct { + App AppConfig + HTTP HTTPConfig +} + +// New returns a new Config Struct +func New() *Config { + return &Config{ + App: AppConfig{ + Name: env("APP_NAME", "Kontor"), + Version: env("APP_VERSION", "v1.0"), + Token: env("APP_TOKEN", ""), + }, + HTTP: HTTPConfig{ + Content: env("HTTP_CONTENT_TYPE", "application/json"), + Problem: env("HTTP_PROBLEM", "application/problem+json"), + Port: env("HTTP_PORT", ":8086"), + }, + } +} + +// env is a simple helper function to read an environment variable or return a default value +func env(key string, defaultValue string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + + return defaultValue +} diff --git a/go/pkg/infrastructure/kernel/app.go b/go/pkg/infrastructure/kernel/app.go new file mode 100644 index 0000000..39eb5b7 --- /dev/null +++ b/go/pkg/infrastructure/kernel/app.go @@ -0,0 +1,91 @@ +package kernel + +import ( + "context" + "net/http" + "time" + + "github.com/google/uuid" + gohandlers "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "gitlab.com/tpeetz-kontor/kontor-go/pkg/infrastructure/app" + "gitlab.com/tpeetz-kontor/kontor-go/pkg/infrastructure/config" + "go.uber.org/zap" +) + +// Boot the Application +func Boot() *app.Application { + // Configuration + config := bootConfig() + + // Router + router := bootRouter() + + // CORS + corsHandler := gohandlers.CORS(gohandlers.AllowedOrigins([]string{"*"})) + + // Logger + logger := bootLogger() + defer logger.Sync() // flushes buffer, if any + + // Create and return and Application + return &app.Application{ + Server: &http.Server{ + Addr: config.HTTP.Port, + Handler: corsHandler(requestIDMiddleware(router)), + IdleTimeout: 120 * time.Second, + ReadTimeout: 1 * time.Second, + WriteTimeout: 1 * time.Second, + }, + Router: router, + Logger: logger, + Config: config, + } +} + +func bootConfig() *config.Config { + return config.New() +} + +func bootRouter() *mux.Router { + return mux.NewRouter() +} + +func bootLogger() *zap.Logger { + logger, logError := zap.NewProduction() + if logError != nil { + panic(logError) + } + + return logger +} + +// ContextKey is used for context.Context value. The value requires a key that is not primitive type. +type ContextKey string + +// ContextKeyRequestID is the ContextKey for RequestID +const ContextKeyRequestID ContextKey = "requestID" + +func requestIDMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + id := uuid.New() + + ctx = context.WithValue(ctx, ContextKeyRequestID, id.String()) + + r = r.WithContext(ctx) + + bootLogger().Debug("Incoming request", + zap.String("method", r.Method), + zap.String("uri", r.RequestURI), + zap.String("addr", r.RemoteAddr), zap.String("id", id.String()), + ) + + next.ServeHTTP(w, r) + + bootLogger().Debug("Finished handling http req. %s", + zap.String("id", id.String())) + }) +} diff --git a/go/pkg/infrastructure/response/response.go b/go/pkg/infrastructure/response/response.go new file mode 100644 index 0000000..f8def4a --- /dev/null +++ b/go/pkg/infrastructure/response/response.go @@ -0,0 +1,20 @@ +package response + +import ( + "encoding/json" + "net/http" +) + +// Response is a generic HTTP Response Struct +type Response struct { + Data string `json:"data"` +} + +// Send a HTTP Response +func Send(responseWriter http.ResponseWriter, code int, payload interface{}, contentType string) { + response, _ := json.Marshal(payload) + + responseWriter.Header().Set("Content-Type", contentType) + responseWriter.WriteHeader(code) + responseWriter.Write(response) +} diff --git a/go/pkg/library/author.go b/go/pkg/library/author.go new file mode 100644 index 0000000..2aafcd7 --- /dev/null +++ b/go/pkg/library/author.go @@ -0,0 +1,12 @@ +package library + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Author defines the data model for library authors with id and name. +type Author struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/library/author_dao.go b/go/pkg/library/author_dao.go new file mode 100644 index 0000000..aa5f189 --- /dev/null +++ b/go/pkg/library/author_dao.go @@ -0,0 +1,74 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// AuthorDAO extends the type BaseDAO. +type AuthorDAO struct { + Db dao.BaseDAO +} + +const ( + // AUTHORCOLLECTION defines the collection name for storing authors. + AUTHORCOLLECTION = "author" + // AUTHORMODEL defines the name of the author data model. + AUTHORMODEL = "kontor.library.author" +) + +// FindAll retrieves the list of authors from the database. +func (m *AuthorDAO) FindAll() ([]Author, error) { + m.Db.Connect() + var authors []Author + err := m.Db.MongoDb.C(AUTHORCOLLECTION).Find(bson.M{"model": AUTHORMODEL}).All(&authors) + return authors, err +} + +// FindByID returns an author with given id or returns the error. +func (m *AuthorDAO) FindByID(id string) (Author, error) { + m.Db.Connect() + var author Author + err := m.Db.MongoDb.C(AUTHORCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&author) + return author, err +} + +// FindByName returns an author with given name or returns the error. +func (m *AuthorDAO) FindByName(name string) (Author, error) { + m.Db.Connect() + var author Author + err := m.Db.MongoDb.C(AUTHORCOLLECTION).Find(bson.M{"name": name, "model": AUTHORMODEL}).One(&author) + return author, err +} + +// Insert a author into database. +func (m *AuthorDAO) Insert(author Author) error { + m.Db.Connect() + author.Model = AUTHORMODEL + err := m.Db.MongoDb.C(AUTHORCOLLECTION).Insert(&author) + return err +} + +// Upsert a author into database. +func (m *AuthorDAO) Upsert(author Author) (*mgo.ChangeInfo, error) { + m.Db.Connect() + author.Model = AUTHORMODEL + info, err := m.Db.MongoDb.C(AUTHORCOLLECTION).Upsert(bson.M{"name": author.Name}, &author) + return info, err +} + +// Delete an existing author. +func (m *AuthorDAO) Delete(author Author) error { + m.Db.Connect() + err := m.Db.MongoDb.C(AUTHORCOLLECTION).Remove(&author) + return err +} + +// Update an existing author. +func (m *AuthorDAO) Update(author Author) error { + m.Db.Connect() + err := m.Db.MongoDb.C(AUTHORCOLLECTION).UpdateId(author.ID, &author) + return err +} diff --git a/go/pkg/library/author_test.go b/go/pkg/library/author_test.go new file mode 100644 index 0000000..54a6537 --- /dev/null +++ b/go/pkg/library/author_test.go @@ -0,0 +1,99 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var authorModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Model", "string"}, +} + +func TestAuthorModel(t *testing.T) { + m := Author{} + if reflect.TypeOf(m).NumField() != len(authorModelTestTable) { + t.Fail() + } + for index, testData := range authorModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListAuthors(t *testing.T) { + var ( + authorDao = AuthorDAO{Db: dao.TestDb} + ) + authors, err := authorDao.FindAll() + if err != nil { + t.Fail() + } + if len(authors) != 0 { + t.Fail() + } +} + +func TestInsertAuthor(t *testing.T) { + var ( + authorDao = AuthorDAO{Db: dao.TestDb} + author = Author{} + authors []Author + ) + author.ID = bson.NewObjectId() + author.Name = "Packt Publishing" + err := authorDao.Insert(author) + if err != nil { + t.Fail() + } + authors, err = authorDao.FindAll() + if len(authors) != 1 { + t.Fail() + } +} + +func TestUpsertAuthor(t *testing.T) { + var ( + authorDao = AuthorDAO{Db: dao.TestDb} + ) + var author = Author{} + author.ID = bson.NewObjectId() + author.Name = "Hansa Verlag" + authorDao.Upsert(author) + authors, err := authorDao.FindAll() + if err != nil { + t.Fail() + } + if len(authors) != 2 { + t.Fail() + } +} + +func TestDeleteAuthor(t *testing.T) { + var ( + authorDao = AuthorDAO{Db: dao.TestDb} + ) + authors, err := authorDao.FindAll() + if err != nil { + t.Fail() + } + for _, author := range authors { + authorDao.Delete(author) + } + authors, err = authorDao.FindAll() + if err != nil { + t.Fail() + } + if len(authors) != 0 { + t.Fail() + } +} diff --git a/go/pkg/library/book.go b/go/pkg/library/book.go new file mode 100644 index 0000000..855f71c --- /dev/null +++ b/go/pkg/library/book.go @@ -0,0 +1,18 @@ +package library + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Book defines the data model for library books with id, title, author, publisher, +// isbn, year and edition. +type Book struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` + Author string `json:"author" bson:"author"` + Publisher bson.ObjectId `json:"publisher" bson:"publisher,omitempty"` + Isbn string `json:"isbn" bson:"isbn,omitempty"` + Year int `json:"year" bson:"year,omitempty"` + Edition string `json:"edition" bson:"edition,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/library/book_dao.go b/go/pkg/library/book_dao.go new file mode 100644 index 0000000..ddfea6a --- /dev/null +++ b/go/pkg/library/book_dao.go @@ -0,0 +1,74 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// BookDAO extends the type BaseDAO. +type BookDAO struct { + Db dao.BaseDAO +} + +const ( + // BOOKCOLLECTION defines the collection name for storing books. + BOOKCOLLECTION = "book" + // BOOKMODEL defines the name of the book data model. + BOOKMODEL = "kontor.library.book" +) + +// FindAll retrieves the list of books from the database. +func (m *BookDAO) FindAll() ([]Book, error) { + m.Db.Connect() + var books []Book + err := m.Db.MongoDb.C(BOOKCOLLECTION).Find(bson.M{"model": BOOKMODEL}).All(&books) + return books, err +} + +// FindByID returns an book with given id or returns the error. +func (m *BookDAO) FindByID(id string) (Book, error) { + m.Db.Connect() + var book Book + err := m.Db.MongoDb.C(BOOKCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&book) + return book, err +} + +// FindByTitle returns a book with given title or returns the error. +func (m *BookDAO) FindByTitle(title string) (Book, error) { + m.Db.Connect() + var book Book + err := m.Db.MongoDb.C(BOOKCOLLECTION).Find(bson.M{"title": title, "model": BOOKMODEL}).One(&book) + return book, err +} + +// Insert a book into database. +func (m *BookDAO) Insert(book Book) error { + m.Db.Connect() + book.Model = BOOKMODEL + err := m.Db.MongoDb.C(BOOKCOLLECTION).Insert(&book) + return err +} + +// Upsert a book into database. +func (m *BookDAO) Upsert(book Book) (*mgo.ChangeInfo, error) { + m.Db.Connect() + book.Model = BOOKMODEL + info, err := m.Db.MongoDb.C(BOOKCOLLECTION).Upsert(bson.M{"title": book.Title}, &book) + return info, err +} + +// Delete an existing book. +func (m *BookDAO) Delete(book Book) error { + m.Db.Connect() + err := m.Db.MongoDb.C(BOOKCOLLECTION).Remove(&book) + return err +} + +// Update an existing book. +func (m *BookDAO) Update(book Book) error { + m.Db.Connect() + err := m.Db.MongoDb.C(BOOKCOLLECTION).UpdateId(book.ID, &book) + return err +} diff --git a/go/pkg/library/book_test.go b/go/pkg/library/book_test.go new file mode 100644 index 0000000..7a4f1f1 --- /dev/null +++ b/go/pkg/library/book_test.go @@ -0,0 +1,107 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var bookModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Title", "string"}, + {"Author", "string"}, + {"Publisher", "string"}, + {"Isbn", "string"}, + {"Year", "int"}, + {"Edition", "string"}, + {"Model", "string"}, +} + +func TestBookModel(t *testing.T) { + m := Book{} + if reflect.TypeOf(m).NumField() != len(bookModelTestTable) { + t.Fail() + } + for index, testData := range bookModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListBooks(t *testing.T) { + var ( + bookDao = BookDAO{Db: dao.TestDb} + ) + books, err := bookDao.FindAll() + if err != nil { + t.Fail() + } + if len(books) != 0 { + t.Fail() + } +} + +func TestInsertBook(t *testing.T) { + var ( + bookDao = BookDAO{Db: dao.TestDb} + book = Book{} + books []Book + ) + book.ID = bson.NewObjectId() + book.Title = "Packt Publishing" + err := bookDao.Insert(book) + if err != nil { + t.Fail() + } + books, err = bookDao.FindAll() + if err != nil { + t.Fail() + } + if len(books) != 1 { + t.Fail() + } +} + +func TestUpsertBook(t *testing.T) { + var ( + bookDao = BookDAO{Db: dao.TestDb} + book = Book{} + ) + book.ID = bson.NewObjectId() + book.Title = "Hansa Verlag" + bookDao.Upsert(book) + books, err := bookDao.FindAll() + if err != nil { + t.Fail() + } + if len(books) != 2 { + t.Fail() + } +} + +func TestDeleteBook(t *testing.T) { + var ( + bookDao = BookDAO{Db: dao.TestDb} + ) + books, err := bookDao.FindAll() + if err != nil { + t.Fail() + } + for _, book := range books { + bookDao.Delete(book) + } + books, err = bookDao.FindAll() + if err != nil { + t.Fail() + } + if len(books) != 0 { + t.Fail() + } +} diff --git a/go/pkg/library/publisher.go b/go/pkg/library/publisher.go new file mode 100644 index 0000000..b7eb84d --- /dev/null +++ b/go/pkg/library/publisher.go @@ -0,0 +1,12 @@ +package library + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Publisher defines the data model for library publishers with id and name. +type Publisher struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Model string `json:"model" bson:"model"` +} diff --git a/go/pkg/library/publisher_dao.go b/go/pkg/library/publisher_dao.go new file mode 100644 index 0000000..0e48cf5 --- /dev/null +++ b/go/pkg/library/publisher_dao.go @@ -0,0 +1,74 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// PublisherDAO extends the type BaseDAO. +type PublisherDAO struct { + Db dao.BaseDAO +} + +const ( + // PUBLISHERCOLLECTION defines the collection name for storing publishers. + PUBLISHERCOLLECTION = "publisher" + // PUBLISHERMODEL defines the name of the publisher data model. + PUBLISHERMODEL = "kontor.library.publisher" +) + +// FindAll retrieves the list of publishers from the database. +func (m *PublisherDAO) FindAll() ([]Publisher, error) { + m.Db.Connect() + var publishers []Publisher + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Find(bson.M{"model": PUBLISHERMODEL}).All(&publishers) + return publishers, err +} + +// FindByID returns an publisher with given id or returns the error. +func (m *PublisherDAO) FindByID(id string) (Publisher, error) { + m.Db.Connect() + var publisher Publisher + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&publisher) + return publisher, err +} + +// FindByName returns an comic with given name or returns the error. +func (m *PublisherDAO) FindByName(name string) (Publisher, error) { + m.Db.Connect() + var publisher Publisher + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Find(bson.M{"name": name, "model": PUBLISHERMODEL}).One(&publisher) + return publisher, err +} + +// Insert a publisher into database. +func (m *PublisherDAO) Insert(publisher Publisher) error { + m.Db.Connect() + publisher.Model = PUBLISHERMODEL + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Insert(&publisher) + return err +} + +// Upsert a publisher into database. +func (m *PublisherDAO) Upsert(publisher Publisher) (*mgo.ChangeInfo, error) { + m.Db.Connect() + publisher.Model = PUBLISHERMODEL + info, err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Upsert(bson.M{"name": publisher.Name}, &publisher) + return info, err +} + +// Delete an existing publisher. +func (m *PublisherDAO) Delete(publisher Publisher) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Remove(&publisher) + return err +} + +// Update an existing publisher. +func (m *PublisherDAO) Update(publisher Publisher) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).UpdateId(publisher.ID, &publisher) + return err +} diff --git a/go/pkg/library/publisher_test.go b/go/pkg/library/publisher_test.go new file mode 100644 index 0000000..1c680f3 --- /dev/null +++ b/go/pkg/library/publisher_test.go @@ -0,0 +1,103 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var publisherModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Model", "string"}, +} + +func TestPublisherModel(t *testing.T) { + m := Publisher{} + if reflect.TypeOf(m).NumField() != len(publisherModelTestTable) { + t.Fail() + } + for index, testData := range publisherModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + + } +} + +func TestListPublishers(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + ) + publishers, err := publisherDao.FindAll() + if err != nil { + t.Fail() + } + if len(publishers) != 0 { + t.Fail() + } +} + +func TestInsertPublisher(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + publisher = Publisher{} + publishers []Publisher + ) + publisher.ID = bson.NewObjectId() + publisher.Name = "Packt Publishing" + err := publisherDao.Insert(publisher) + if err != nil { + t.Fail() + } + publishers, err = publisherDao.FindAll() + if err != nil { + t.Fail() + } + if len(publishers) != 1 { + t.Fail() + } +} + +func TestUpsertPublisher(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + publisher = Publisher{} + ) + publisher.ID = bson.NewObjectId() + publisher.Name = "Hansa Verlag" + publisherDao.Upsert(publisher) + publishers, err := publisherDao.FindAll() + if err != nil { + t.Fail() + } + if len(publishers) != 2 { + t.Fail() + } +} + +func TestDeletePublisher(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + ) + publishers, err := publisherDao.FindAll() + if err != nil { + t.Fail() + } + for _, publisher := range publishers { + publisherDao.Delete(publisher) + } + publishers, err = publisherDao.FindAll() + if err != nil { + t.Fail() + } + if len(publishers) != 0 { + t.Fail() + } +} diff --git a/go/pkg/library/publisher_views.go b/go/pkg/library/publisher_views.go new file mode 100644 index 0000000..233169a --- /dev/null +++ b/go/pkg/library/publisher_views.go @@ -0,0 +1,68 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + "net/http" + + "github.com/gin-gonic/gin" +) + +const ( + // LibraryPublisherTemplate defines name of template file for comics publishers + LibraryPublisherTemplate = "library/publishers.html" +) + +func showPublisherList(c *gin.Context) { + var dao = PublisherDAO{Db: dao.KontorDb} + if publishers, err := dao.FindAll(); err == nil { + util.Render(c, gin.H{"title": "Library Publisher List", "payload": publishers}, LibraryPublisherTemplate) + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} + +func showPublisherDetails(c *gin.Context) { + var dao = PublisherDAO{Db: dao.KontorDb} + publisherid := c.Param("publisher_id") + if publisher, err := dao.FindByID(publisherid); err == nil { + util.Render(c, gin.H{"title": "Library Publisher", "payload": publisher, "action": util.SaveAction}, LibraryPublisherTemplate) + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} + +func showPublisherCreation(c *gin.Context) { + var publisher = Publisher{} + util.Render(c, gin.H{"title": "Library Publisher Creation", "payload": publisher, "action": util.AddAction}, LibraryPublisherTemplate) +} + +func validatePublisherDetails(c *gin.Context) { + name := c.PostForm("name") + action := c.PostForm("action") + publisherid := c.PostForm("publisherid") + + var err error + var dao = PublisherDAO{Db: dao.KontorDb} + var publisher = Publisher{} + + switch action { + case util.AddAction: + publisher.Name = name + _, err = dao.Upsert(publisher) + case util.SaveAction: + publisher, _ = dao.FindByID(publisherid) + publisher.Name = name + err = dao.Update(publisher) + case util.DeleteAction: + publisher, _ = dao.FindByID(publisherid) + err = dao.Delete(publisher) + } + if err == nil { + c.Redirect(http.StatusTemporaryRedirect, "/library/publisher") + } else { + c.HTML(http.StatusBadRequest, "library/publisher.html", gin.H{ + "ErrorTitle": "Publisher Creation Failed", + "ErrorMessage": err.Error()}) + } +} diff --git a/go/pkg/library/routes.go b/go/pkg/library/routes.go new file mode 100644 index 0000000..ea93fd5 --- /dev/null +++ b/go/pkg/library/routes.go @@ -0,0 +1,22 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + + "github.com/gin-gonic/gin" +) + +// GetRoutes returns all routes for library related data. +func GetRoutes(router *gin.Engine) { + libraryRoutes := router.Group("/library") + { + libraryRoutes.GET("/", auth.EnsureLoggedIn(), showBookList) + libraryRoutes.GET("/book", auth.EnsureLoggedIn(), showBookList) + libraryRoutes.GET("/author", auth.EnsureLoggedIn(), showAuthorList) + libraryRoutes.GET("/publisher", auth.EnsureLoggedIn(), showPublisherList) + libraryRoutes.POST("/publisher", auth.EnsureLoggedIn(), showPublisherList) + libraryRoutes.GET("/publisher/view/:publisher_id", auth.EnsureLoggedIn(), showPublisherDetails) + libraryRoutes.POST("/publisher/validate", auth.EnsureLoggedIn(), validatePublisherDetails) + libraryRoutes.GET("/publisher/create", auth.EnsureLoggedIn(), showPublisherCreation) + } +} diff --git a/go/pkg/library/views.go b/go/pkg/library/views.go new file mode 100644 index 0000000..4b4bcc6 --- /dev/null +++ b/go/pkg/library/views.go @@ -0,0 +1,24 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + + "github.com/gin-gonic/gin" +) + +func showAuthorList(c *gin.Context) { + var authorDao = AuthorDAO{Db: dao.KontorDb} + if authors, err := authorDao.FindAll(); err == nil { + util.Render(c, gin.H{"title": "Author List", "payload": authors}, "library/authors.html") + } +} + +func showBookList(c *gin.Context) { + var bookDao = BookDAO{Db: dao.KontorDb} + if books, err := bookDao.FindAll(); err == nil { + util.Render(c, gin.H{"title": "Book List", "payload": books}, "library/books.html") + } else { + util.Render(c, gin.H{"title": "Kontor", "payload": nil}, "kontor/index.html") + } +} diff --git a/go/pkg/office/routes.go b/go/pkg/office/routes.go new file mode 100644 index 0000000..8674071 --- /dev/null +++ b/go/pkg/office/routes.go @@ -0,0 +1,13 @@ +package office + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + + "github.com/gin-gonic/gin" +) + +// GetRoutes returns all routes for office related data. +func GetRoutes(router *gin.Engine) { + officeRoutes := router.Group("/office") + officeRoutes.GET("/", auth.EnsureLoggedIn(), showIndexPage) +} diff --git a/go/pkg/office/views.go b/go/pkg/office/views.go new file mode 100644 index 0000000..2fd1504 --- /dev/null +++ b/go/pkg/office/views.go @@ -0,0 +1,11 @@ +package office + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + + "github.com/gin-gonic/gin" +) + +func showIndexPage(c *gin.Context) { + util.Render(c, gin.H{"title": "Home Office", "payload": nil}, "office/index.html") +} diff --git a/go/pkg/properties/root.go b/go/pkg/properties/root.go new file mode 100644 index 0000000..c80e136 --- /dev/null +++ b/go/pkg/properties/root.go @@ -0,0 +1,25 @@ +package properties + +var ( + // Version defines the version of the web application kontor. + Version = "undefined" + // Debug defines the property debug to be used for more verbose output. + Debug = false + // Port defines port number under the web application is reachable. + Port = 8500 +) + +// SetVersion sets Version with given value. +func SetVersion(value string) { + Version = value +} + +// SetDebug sets Debug with given value. +func SetDebug(value bool) { + Debug = value +} + +// SetPort sets Port with given value. +func SetPort(value int) { + Port = value +} diff --git a/go/pkg/setup/data.go b/go/pkg/setup/data.go new file mode 100644 index 0000000..e45e813 --- /dev/null +++ b/go/pkg/setup/data.go @@ -0,0 +1,20 @@ +package setup + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/tysc" + "log" +) + +// CheckTradeYourSportsCardsData checks if the TYSC releated data is available. +func CheckTradeYourSportsCardsData() { + log.Printf("Check data for TradeYourSportsCards module") + var sport = tysc.SportDAO{Db: dao.KontorDb} + sport.Upsert(tysc.Sport{Name: "Football"}) + football, _ := sport.FindByName("Football") + sport.Upsert(tysc.Sport{Name: "Baseball"}) + sport.Upsert(tysc.Sport{Name: "Basketball"}) + sport.Upsert(tysc.Sport{Name: "Hockey"}) + var position = tysc.PositionDAO{Db: dao.KontorDb} + position.Upsert(tysc.Position{Name: "WR", Description: "Wide Receiver", Sport: football.ID}) +} diff --git a/go/pkg/setup/routes.go b/go/pkg/setup/routes.go new file mode 100644 index 0000000..3b1a9b6 --- /dev/null +++ b/go/pkg/setup/routes.go @@ -0,0 +1,38 @@ +package setup + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/admin" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/comics" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/library" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/office" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/tradingcards" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/tysc" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + + "github.com/gin-gonic/gin" +) + +// InitializeRoutes setup the routes for Kontor web application. +func InitializeRoutes(router *gin.Engine) { + + // Use the setUserStatus middleware for every route to set a flag + // indicating whether the request was from an authenticated user or not + router.Use(auth.SetSessionStatus()) + + // Handle the index route + router.GET("/", util.ShowIndexPage) + + userRoutes := router.Group("/user") + { + userRoutes.GET("/login", auth.EnsureNotLoggedIn(), admin.ShowLoginPage) + userRoutes.POST("/login", auth.EnsureNotLoggedIn(), admin.PerformLogin) + userRoutes.GET("/logout", auth.EnsureLoggedIn(), admin.Logout) + } + admin.GetRoutes(router) + comics.GetRoutes(router) + library.GetRoutes(router) + office.GetRoutes(router) + tradingcards.GetRoutes(router) + tysc.GetRoutes(router) +} diff --git a/go/pkg/setup/session.go b/go/pkg/setup/session.go new file mode 100644 index 0000000..5fe184a --- /dev/null +++ b/go/pkg/setup/session.go @@ -0,0 +1,15 @@ +package setup + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" +) + +// CleanupSessions removes all sessions from database. +func CleanupSessions() { + sessionDao := auth.SessionDAO{Db: dao.KontorDb} + sessions, _ := sessionDao.FindAll() + for _, session := range sessions { + sessionDao.Delete(session) + } +} diff --git a/go/pkg/setup/user.go b/go/pkg/setup/user.go new file mode 100644 index 0000000..5272ed3 --- /dev/null +++ b/go/pkg/setup/user.go @@ -0,0 +1,20 @@ +package setup + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/admin" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + "gopkg.in/mgo.v2/bson" +) + +// CheckUserList ensures that at least the admin user is available. +func CheckUserList() { + var userDao = admin.UserDAO{Db: dao.KontorDb} + users, err := userDao.FindAll() + if err == nil && len(users) == 0 { + password, _ := admin.HashPassword("admin") + id := bson.NewObjectId() + user := admin.User{ID: id, Username: "admin", Password: password, Firstname: "Administrator", IsAdmin: true} + userDao.Insert(user) + } +} diff --git a/go/pkg/tradingcards/routes.go b/go/pkg/tradingcards/routes.go new file mode 100644 index 0000000..d5d3aad --- /dev/null +++ b/go/pkg/tradingcards/routes.go @@ -0,0 +1,13 @@ +package tradingcards + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + + "github.com/gin-gonic/gin" +) + +// GetRoutes returns all routes for tradingcards related data. +func GetRoutes(router *gin.Engine) { + tradingCardsRoutes := router.Group("/tradingcards") + tradingCardsRoutes.GET("/", auth.EnsureLoggedIn(), showIndexPage) +} diff --git a/go/pkg/tradingcards/views.go b/go/pkg/tradingcards/views.go new file mode 100644 index 0000000..1d4f104 --- /dev/null +++ b/go/pkg/tradingcards/views.go @@ -0,0 +1,11 @@ +package tradingcards + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + + "github.com/gin-gonic/gin" +) + +func showIndexPage(c *gin.Context) { + util.Render(c, gin.H{"title": "Trading Cards", "payload": nil}, "tradingcards/index.html") +} diff --git a/go/pkg/tysc/card.go b/go/pkg/tysc/card.go new file mode 100644 index 0000000..e2555b5 --- /dev/null +++ b/go/pkg/tysc/card.go @@ -0,0 +1,20 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Card defines the data model for TYSC cards. +type Card struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Player bson.ObjectId `json:"player" bson:"player,omitempty"` + Team bson.ObjectId `json:"team" bson:"team,omitempty"` + Manufacturer bson.ObjectId `json:"manufacturer" bson:"manufacturer,omitempty"` + CardSet bson.ObjectId `json:"cardset" bson:"cardset,omitempty"` + ParallelSet bson.ObjectId `json:"parallelset" bson:"paralelset,omitempty"` + InsertSet bson.ObjectId `json:"insertset" bson:"insertset,omitempty"` + Rookie bool `json:"rookie" bson:"rookie,omitempty"` + Year int `json:"year" bson:"year,omitempty"` + Number int `json:"number" bson:"number,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/card_dao.go b/go/pkg/tysc/card_dao.go new file mode 100644 index 0000000..8fa4d4b --- /dev/null +++ b/go/pkg/tysc/card_dao.go @@ -0,0 +1,66 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// CardDAO extends the type BaseDAO. +type CardDAO struct { + Db dao.BaseDAO +} + +const ( + // CARDCOLLECTION defines the collection name for storing cards. + CARDCOLLECTION = "card" + // CARDMODEL defines the name of the card data model. + CARDMODEL = "kontor.tysc.card" +) + +// FindAll retrieves the list of cards from the database. +func (m *CardDAO) FindAll() ([]Card, error) { + m.Db.Connect() + var cards []Card + err := m.Db.MongoDb.C(CARDCOLLECTION).Find(bson.M{"model": CARDMODEL}).All(&cards) + return cards, err +} + +// FindByID returns an card with given id or returns the error. +func (m *CardDAO) FindByID(id string) (Card, error) { + m.Db.Connect() + var card Card + err := m.Db.MongoDb.C(CARDCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&card) + return card, err +} + +// Insert a card into database. +func (m *CardDAO) Insert(card Card) error { + m.Db.Connect() + card.Model = CARDMODEL + err := m.Db.MongoDb.C(CARDCOLLECTION).Insert(&card) + return err +} + +// Upsert a card into database. +func (m *CardDAO) Upsert(card Card) (*mgo.ChangeInfo, error) { + m.Db.Connect() + card.Model = CARDMODEL + info, err := m.Db.MongoDb.C(CARDCOLLECTION).Upsert(bson.M{"number": card.Number}, &card) + return info, err +} + +// Update an existing card. +func (m *CardDAO) Update(card Card) error { + m.Db.Connect() + err := m.Db.MongoDb.C(CARDCOLLECTION).UpdateId(card.ID, &card) + return err +} + +// Delete an existing card. +func (m *CardDAO) Delete(card Card) error { + m.Db.Connect() + err := m.Db.MongoDb.C(CARDCOLLECTION).Remove(&card) + return err +} diff --git a/go/pkg/tysc/card_test.go b/go/pkg/tysc/card_test.go new file mode 100644 index 0000000..e5c0afb --- /dev/null +++ b/go/pkg/tysc/card_test.go @@ -0,0 +1,110 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var cardModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Player", "string"}, + {"Team", "string"}, + {"Manufacturer", "string"}, + {"CardSet", "string"}, + {"ParallelSet", "string"}, + {"InsertSet", "string"}, + {"Rookie", "bool"}, + {"Year", "int"}, + {"Number", "int"}, + {"Model", "string"}, +} + +func TestCardModel(t *testing.T) { + m := Card{} + if reflect.TypeOf(m).NumField() != len(cardModelTestTable) { + t.Fail() + } + for index, testData := range cardModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListCards(t *testing.T) { + var ( + cardDao = CardDAO{Db: dao.TestDb} + ) + cards, err := cardDao.FindAll() + if err != nil { + t.Fail() + } + if len(cards) != 0 { + t.Fail() + } +} + +func TestInsertCard(t *testing.T) { + var ( + cardDao = CardDAO{Db: dao.TestDb} + card = Card{} + cards []Card + ) + card.ID = bson.NewObjectId() + card.Number = 1 + err := cardDao.Insert(card) + if err != nil { + t.Fail() + } + cards, err = cardDao.FindAll() + if err != nil { + t.Fail() + } + if len(cards) != 1 { + t.Fail() + } +} + +func TestUpsertCard(t *testing.T) { + var ( + cardDao = CardDAO{Db: dao.TestDb} + card = Card{} + ) + card.ID = bson.NewObjectId() + card.Number = 2 + cardDao.Upsert(card) + cards, err := cardDao.FindAll() + if err != nil { + t.Fail() + } + if len(cards) != 2 { + t.Fail() + } +} + +func TestDeleteCard(t *testing.T) { + var ( + cardDao = CardDAO{Db: dao.TestDb} + ) + cards, err := cardDao.FindAll() + if err != nil { + t.Fail() + } + for _, card := range cards { + cardDao.Delete(card) + } + cards, err = cardDao.FindAll() + if err != nil { + t.Fail() + } + if len(cards) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/cardset.go b/go/pkg/tysc/cardset.go new file mode 100644 index 0000000..6184a11 --- /dev/null +++ b/go/pkg/tysc/cardset.go @@ -0,0 +1,13 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// CardSet defines the data model for TYSC card sets with id, name and manufacturer. +type CardSet struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Manufacturer bson.ObjectId `json:"manufacturer" bson:"manufacturer,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/cardset_dao.go b/go/pkg/tysc/cardset_dao.go new file mode 100644 index 0000000..4158860 --- /dev/null +++ b/go/pkg/tysc/cardset_dao.go @@ -0,0 +1,82 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// CardSetDAO extends the type BaseDAO. +type CardSetDAO struct { + Db dao.BaseDAO +} + +const ( + // CARDSETCOLLECTION defines the collection name for storing cards sets. + CARDSETCOLLECTION = "cardSet" + // CARDSETMODEL defines the name of the card set data model. + CARDSETMODEL = "kontor.tysc.cardSet" +) + +// FindAll retrieves the list of card sets from the database. +func (m *CardSetDAO) FindAll() ([]CardSet, error) { + m.Db.Connect() + var cardSets []CardSet + err := m.Db.MongoDb.C(CARDSETCOLLECTION).Find(bson.M{"model": CARDSETMODEL}).All(&cardSets) + return cardSets, err +} + +// FindByID returns a card set with given id or returns the error. +func (m *CardSetDAO) FindByID(id string) (CardSet, error) { + m.Db.Connect() + var cardSet CardSet + err := m.Db.MongoDb.C(CARDSETCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&cardSet) + return cardSet, err +} + +// FindByManufacturer returns a card set with given manufacturer or returns the error. +func (m *CardSetDAO) FindByManufacturer(manufacturer string) ([]CardSet, error) { + m.Db.Connect() + var cardSets []CardSet + err := m.Db.MongoDb.C(CARDSETCOLLECTION).Find(bson.M{"model": CARDSETMODEL, "manufacturer": bson.ObjectIdHex(manufacturer)}).All(&cardSets) + return cardSets, err +} + +// FindByName returns a card set with given name or returns the error. +func (m *CardSetDAO) FindByName(name string) (CardSet, error) { + m.Db.Connect() + var cardSet CardSet + err := m.Db.MongoDb.C(CARDSETCOLLECTION).Find(bson.M{"name": name, "model": CARDSETMODEL}).One(&cardSet) + return cardSet, err +} + +// Insert an card set into database. +func (m *CardSetDAO) Insert(cardSet CardSet) error { + m.Db.Connect() + cardSet.Model = CARDSETMODEL + err := m.Db.MongoDb.C(CARDSETCOLLECTION).Insert(&cardSet) + return err +} + +// Upsert an card set into database. +func (m *CardSetDAO) Upsert(cardSet CardSet) (*mgo.ChangeInfo, error) { + m.Db.Connect() + cardSet.Model = CARDSETMODEL + info, err := m.Db.MongoDb.C(CARDSETCOLLECTION).Upsert(bson.M{"name": cardSet.Name}, &cardSet) + return info, err +} + +// Update an existing card set. +func (m *CardSetDAO) Update(cardSet CardSet) error { + m.Db.Connect() + err := m.Db.MongoDb.C(CARDSETCOLLECTION).UpdateId(cardSet.ID, &cardSet) + return err +} + +// Delete an existing card set. +func (m *CardSetDAO) Delete(cardSet CardSet) error { + m.Db.Connect() + err := m.Db.MongoDb.C(CARDSETCOLLECTION).Remove(&cardSet) + return err +} diff --git a/go/pkg/tysc/cardset_test.go b/go/pkg/tysc/cardset_test.go new file mode 100644 index 0000000..82d7ebc --- /dev/null +++ b/go/pkg/tysc/cardset_test.go @@ -0,0 +1,103 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var cardsetModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Manufacturer", "string"}, + {"Model", "string"}, +} + +func TestCardSetModel(t *testing.T) { + m := CardSet{} + if reflect.TypeOf(m).NumField() != len(cardsetModelTestTable) { + t.Fail() + } + for index, testData := range cardsetModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListCardSets(t *testing.T) { + var ( + cardsetDao = CardSetDAO{Db: dao.TestDb} + ) + cardSets, err := cardsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(cardSets) != 0 { + t.Fail() + } +} + +func TestInsertCardSet(t *testing.T) { + var ( + cardsetDao = CardSetDAO{Db: dao.TestDb} + cardSet = CardSet{} + cardSets []CardSet + ) + cardSet.ID = bson.NewObjectId() + cardSet.Name = "test" + err := cardsetDao.Insert(cardSet) + if err != nil { + t.Fail() + } + cardSets, err = cardsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(cardSets) != 1 { + t.Fail() + } +} + +func TestUpsertCardSet(t *testing.T) { + var ( + cardsetDao = CardSetDAO{Db: dao.TestDb} + cardSet = CardSet{} + ) + cardSet.ID = bson.NewObjectId() + cardSet.Name = "test2" + cardsetDao.Upsert(cardSet) + cardSets, err := cardsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(cardSets) != 2 { + t.Fail() + } +} + +func TestDeleteCardSet(t *testing.T) { + var ( + cardsetDao = CardSetDAO{Db: dao.TestDb} + ) + cardSets, err := cardsetDao.FindAll() + if err != nil { + t.Fail() + } + for _, cardSet := range cardSets { + cardsetDao.Delete(cardSet) + } + cardSets, err = cardsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(cardSets) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/insertset.go b/go/pkg/tysc/insertset.go new file mode 100644 index 0000000..8655d35 --- /dev/null +++ b/go/pkg/tysc/insertset.go @@ -0,0 +1,13 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// InsertSet defines the data model for TYSC inserts sets with id, name and manufacturer. +type InsertSet struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Manufacturer bson.ObjectId `json:"manufacturer" bson:"manufacturer,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/insertset_dao.go b/go/pkg/tysc/insertset_dao.go new file mode 100644 index 0000000..dc87ddf --- /dev/null +++ b/go/pkg/tysc/insertset_dao.go @@ -0,0 +1,82 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// InsertSetDAO extends the type BaseDAO. +type InsertSetDAO struct { + Db dao.BaseDAO +} + +const ( + // INSERTSETCOLLECTION defines the collection name for storing insert sets. + INSERTSETCOLLECTION = "insertSet" + // INSERTSETMODEL defines the name of the insert set data model. + INSERTSETMODEL = "kontor.tysc.insertSet" +) + +// FindAll retrieves the list of cards from the database. +func (m *InsertSetDAO) FindAll() ([]InsertSet, error) { + m.Db.Connect() + var insertSets []InsertSet + err := m.Db.MongoDb.C(INSERTSETCOLLECTION).Find(bson.M{"model": INSERTSETMODEL}).All(&insertSets) + return insertSets, err +} + +// FindByID returns an card with given id or returns the error. +func (m *InsertSetDAO) FindByID(id string) (InsertSet, error) { + m.Db.Connect() + var insertSet InsertSet + err := m.Db.MongoDb.C(INSERTSETCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&insertSet) + return insertSet, err +} + +// FindByManufacturer returns a insert set with given manufacturer or returns the error. +func (m *InsertSetDAO) FindByManufacturer(manufacturer string) ([]InsertSet, error) { + m.Db.Connect() + var insertSets []InsertSet + err := m.Db.MongoDb.C(INSERTSETCOLLECTION).Find(bson.M{"model": INSERTSETMODEL, "manufacturer": bson.ObjectIdHex(manufacturer)}).All(&insertSets) + return insertSets, err +} + +// FindByName returns a insert set with given name or returns the error. +func (m *InsertSetDAO) FindByName(name string) (InsertSet, error) { + m.Db.Connect() + var insertSet InsertSet + err := m.Db.MongoDb.C(INSERTSETCOLLECTION).Find(bson.M{"name": name, "model": INSERTSETMODEL}).One(&insertSet) + return insertSet, err +} + +// Insert an insert set into database. +func (m *InsertSetDAO) Insert(insertSet InsertSet) error { + m.Db.Connect() + insertSet.Model = INSERTSETMODEL + err := m.Db.MongoDb.C(INSERTSETCOLLECTION).Insert(&insertSet) + return err +} + +// Upsert an insert set into database. +func (m *InsertSetDAO) Upsert(insertSet InsertSet) (*mgo.ChangeInfo, error) { + m.Db.Connect() + insertSet.Model = INSERTSETMODEL + info, err := m.Db.MongoDb.C(INSERTSETCOLLECTION).Upsert(bson.M{"name": insertSet.Name}, &insertSet) + return info, err +} + +// Update an existing insert set. +func (m *InsertSetDAO) Update(insertSet InsertSet) error { + m.Db.Connect() + err := m.Db.MongoDb.C(INSERTSETCOLLECTION).UpdateId(insertSet.ID, &insertSet) + return err +} + +// Delete an existing insert set. +func (m *InsertSetDAO) Delete(insertSet InsertSet) error { + m.Db.Connect() + err := m.Db.MongoDb.C(INSERTSETCOLLECTION).Remove(&insertSet) + return err +} diff --git a/go/pkg/tysc/insertset_test.go b/go/pkg/tysc/insertset_test.go new file mode 100644 index 0000000..b4d4152 --- /dev/null +++ b/go/pkg/tysc/insertset_test.go @@ -0,0 +1,103 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var insertsetModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Manufacturer", "string"}, + {"Model", "string"}, +} + +func TestInsertSetModel(t *testing.T) { + m := InsertSet{} + if reflect.TypeOf(m).NumField() != len(insertsetModelTestTable) { + t.Fail() + } + for index, testData := range insertsetModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListInsertSets(t *testing.T) { + var ( + insertsetDao = InsertSetDAO{Db: dao.TestDb} + ) + insertSets, err := insertsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(insertSets) != 0 { + t.Fail() + } +} + +func TestInsertInsertSet(t *testing.T) { + var ( + insertsetDao = InsertSetDAO{Db: dao.TestDb} + insertSet = InsertSet{} + insertSets []InsertSet + ) + insertSet.ID = bson.NewObjectId() + insertSet.Name = "test" + err := insertsetDao.Insert(insertSet) + if err != nil { + t.Fail() + } + insertSets, err = insertsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(insertSets) != 1 { + t.Fail() + } +} + +func TestUpsertInsertSet(t *testing.T) { + var ( + insertsetDao = InsertSetDAO{Db: dao.TestDb} + insertSet = InsertSet{} + ) + insertSet.ID = bson.NewObjectId() + insertSet.Name = "test2" + insertsetDao.Upsert(insertSet) + insertSets, err := insertsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(insertSets) != 2 { + t.Fail() + } +} + +func TestDeleteInsertSet(t *testing.T) { + var ( + insertsetDao = InsertSetDAO{Db: dao.TestDb} + ) + insertSets, err := insertsetDao.FindAll() + if err != nil { + t.Fail() + } + for _, insertSet := range insertSets { + insertsetDao.Delete(insertSet) + } + insertSets, err = insertsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(insertSets) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/manufacturer.go b/go/pkg/tysc/manufacturer.go new file mode 100644 index 0000000..e4ccdc0 --- /dev/null +++ b/go/pkg/tysc/manufacturer.go @@ -0,0 +1,12 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Manufacturer defines the data model for TYSC manufacturers with id, and name. +type Manufacturer struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/manufacturer_dao.go b/go/pkg/tysc/manufacturer_dao.go new file mode 100644 index 0000000..b23be11 --- /dev/null +++ b/go/pkg/tysc/manufacturer_dao.go @@ -0,0 +1,74 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// ManufacturerDAO extends the type BaseDAO. +type ManufacturerDAO struct { + Db dao.BaseDAO +} + +const ( + // MANUFACTURERCOLLECTION defines the collection name for storing manufacturers. + MANUFACTURERCOLLECTION = "manufacturer" + // MANUFACTURERMODEL defines the name of the manufacturer data model. + MANUFACTURERMODEL = "kontor.tysc.manufacturer" +) + +// FindAll retrieves the list of manufacturers from the database. +func (m *ManufacturerDAO) FindAll() ([]Manufacturer, error) { + m.Db.Connect() + var manufacturers []Manufacturer + err := m.Db.MongoDb.C(MANUFACTURERCOLLECTION).Find(bson.M{"model": MANUFACTURERMODEL}).All(&manufacturers) + return manufacturers, err +} + +// FindByID returns a manufacturer with given id or returns the error. +func (m *ManufacturerDAO) FindByID(id string) (Manufacturer, error) { + m.Db.Connect() + var manufacturer Manufacturer + err := m.Db.MongoDb.C(MANUFACTURERCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&manufacturer) + return manufacturer, err +} + +// FindByName returns a manufacturer with given name or returns the error. +func (m *ManufacturerDAO) FindByName(name string) (Manufacturer, error) { + m.Db.Connect() + var manufacturer Manufacturer + err := m.Db.MongoDb.C(MANUFACTURERCOLLECTION).Find(bson.M{"name": name, "model": MANUFACTURERMODEL}).One(&manufacturer) + return manufacturer, err +} + +// Insert a manufacturer into database. +func (m *ManufacturerDAO) Insert(manufacturer Manufacturer) error { + m.Db.Connect() + manufacturer.Model = MANUFACTURERMODEL + err := m.Db.MongoDb.C(MANUFACTURERCOLLECTION).Insert(&manufacturer) + return err +} + +// Upsert a manufacturer into database. +func (m *ManufacturerDAO) Upsert(manufacturer Manufacturer) (*mgo.ChangeInfo, error) { + m.Db.Connect() + manufacturer.Model = MANUFACTURERMODEL + info, err := m.Db.MongoDb.C(MANUFACTURERCOLLECTION).Upsert(bson.M{"name": manufacturer.Name}, &manufacturer) + return info, err +} + +// Update an existing manufacturer. +func (m *ManufacturerDAO) Update(manufacturer Manufacturer) error { + m.Db.Connect() + err := m.Db.MongoDb.C(MANUFACTURERCOLLECTION).UpdateId(manufacturer.ID, &manufacturer) + return err +} + +// Delete an existing manufacturer. +func (m *ManufacturerDAO) Delete(manufacturer Manufacturer) error { + m.Db.Connect() + err := m.Db.MongoDb.C(MANUFACTURERCOLLECTION).Remove(&manufacturer) + return err +} diff --git a/go/pkg/tysc/manufacturer_test.go b/go/pkg/tysc/manufacturer_test.go new file mode 100644 index 0000000..9973beb --- /dev/null +++ b/go/pkg/tysc/manufacturer_test.go @@ -0,0 +1,102 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var manufacturerModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Model", "string"}, +} + +func TestManufacturerModel(t *testing.T) { + m := Manufacturer{} + if reflect.TypeOf(m).NumField() != len(manufacturerModelTestTable) { + t.Fail() + } + for index, testData := range manufacturerModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListManufacturers(t *testing.T) { + var ( + manufacturerDao = ManufacturerDAO{Db: dao.TestDb} + ) + manufacturers, err := manufacturerDao.FindAll() + if err != nil { + t.Fail() + } + if len(manufacturers) != 0 { + t.Fail() + } +} + +func TestInsertManufacturer(t *testing.T) { + var ( + manufacturerDao = ManufacturerDAO{Db: dao.TestDb} + manufacturer = Manufacturer{} + manufacturers []Manufacturer + ) + manufacturer.ID = bson.NewObjectId() + manufacturer.Name = "test" + err := manufacturerDao.Insert(manufacturer) + if err != nil { + t.Fail() + } + manufacturers, err = manufacturerDao.FindAll() + if err != nil { + t.Fail() + } + if len(manufacturers) != 1 { + t.Fail() + } +} + +func TestUpsertManufacturer(t *testing.T) { + var ( + manufacturerDao = ManufacturerDAO{Db: dao.TestDb} + manufacturer = Manufacturer{} + ) + manufacturer.ID = bson.NewObjectId() + manufacturer.Name = "test2" + manufacturerDao.Upsert(manufacturer) + manufacturers, err := manufacturerDao.FindAll() + if err != nil { + t.Fail() + } + if len(manufacturers) != 2 { + t.Fail() + } +} + +func TestDeleteManufacturer(t *testing.T) { + var ( + manufacturerDao = ManufacturerDAO{Db: dao.TestDb} + ) + manufacturers, err := manufacturerDao.FindAll() + if err != nil { + t.Fail() + } + for _, manufacturer := range manufacturers { + manufacturerDao.Delete(manufacturer) + } + manufacturers, err = manufacturerDao.FindAll() + if err != nil { + t.Fail() + } + if len(manufacturers) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/parallelset.go b/go/pkg/tysc/parallelset.go new file mode 100644 index 0000000..78a93e2 --- /dev/null +++ b/go/pkg/tysc/parallelset.go @@ -0,0 +1,13 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// ParallelSet defines the data model for TYSC parallel sets with id, name and manufacturer. +type ParallelSet struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Manufacturer bson.ObjectId `json:"manufacturer" bson:"manufacturer,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/parallelset_dao.go b/go/pkg/tysc/parallelset_dao.go new file mode 100644 index 0000000..0cadac3 --- /dev/null +++ b/go/pkg/tysc/parallelset_dao.go @@ -0,0 +1,82 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// ParallelSetDAO extends the type BaseDAO. +type ParallelSetDAO struct { + Db dao.BaseDAO +} + +const ( + // PARALLELSETCOLLECTION defines the collection name for storing parallel sets. + PARALLELSETCOLLECTION = "parallelSet" + // PARALLELSETMODEL defines the name of the parallel set data model. + PARALLELSETMODEL = "kontor.tysc.parallelSet" +) + +// FindAll retrieves the list of parallel sets from the database. +func (m *ParallelSetDAO) FindAll() ([]ParallelSet, error) { + m.Db.Connect() + var parallelSets []ParallelSet + err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).Find(bson.M{"model": PARALLELSETMODEL}).All(¶llelSets) + return parallelSets, err +} + +// FindByID returns a parallel set with given id or returns the error. +func (m *ParallelSetDAO) FindByID(id string) (ParallelSet, error) { + m.Db.Connect() + var parallelSet ParallelSet + err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).FindId(bson.ObjectIdHex(id)).One(¶llelSet) + return parallelSet, err +} + +// FindByManufacturer returns a paralle set with given manufacturer or returns the error. +func (m *ParallelSetDAO) FindByManufacturer(manufacturer string) ([]ParallelSet, error) { + m.Db.Connect() + var parallelSets []ParallelSet + err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).Find(bson.M{"model": PARALLELSETMODEL, "manufacturer": bson.ObjectIdHex(manufacturer)}).All(¶llelSets) + return parallelSets, err +} + +// FindByName returns a parallel set with given name or returns the error. +func (m *ParallelSetDAO) FindByName(name string) (ParallelSet, error) { + m.Db.Connect() + var parallelSet ParallelSet + err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).Find(bson.M{"name": name, "model": PARALLELSETMODEL}).One(¶llelSet) + return parallelSet, err +} + +// Insert a parallel set into database. +func (m *ParallelSetDAO) Insert(parallelSet ParallelSet) error { + m.Db.Connect() + parallelSet.Model = PARALLELSETMODEL + err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).Insert(¶llelSet) + return err +} + +// Upsert a parallel set into database. +func (m *ParallelSetDAO) Upsert(parallelSet ParallelSet) (*mgo.ChangeInfo, error) { + m.Db.Connect() + parallelSet.Model = PARALLELSETMODEL + info, err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).Upsert(bson.M{"name": parallelSet.Name}, ¶llelSet) + return info, err +} + +// Update an existing parallel set. +func (m *ParallelSetDAO) Update(parallelSet ParallelSet) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).UpdateId(parallelSet.ID, ¶llelSet) + return err +} + +// Delete an existing parallel set. +func (m *ParallelSetDAO) Delete(parallelSet ParallelSet) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).Remove(¶llelSet) + return err +} diff --git a/go/pkg/tysc/parallelset_test.go b/go/pkg/tysc/parallelset_test.go new file mode 100644 index 0000000..e0a9c2b --- /dev/null +++ b/go/pkg/tysc/parallelset_test.go @@ -0,0 +1,103 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var parallelsetModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Manufacturer", "string"}, + {"Model", "string"}, +} + +func TestParallelSetModel(t *testing.T) { + m := ParallelSet{} + if reflect.TypeOf(m).NumField() != len(parallelsetModelTestTable) { + t.Fail() + } + for index, testData := range parallelsetModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListParallelSets(t *testing.T) { + var ( + parallelsetDao = ParallelSetDAO{Db: dao.TestDb} + ) + parallelSets, err := parallelsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(parallelSets) != 0 { + t.Fail() + } +} + +func TestInsertParallelSet(t *testing.T) { + var ( + parallelsetDao = ParallelSetDAO{Db: dao.TestDb} + parallelSet = ParallelSet{} + parallelSets []ParallelSet + ) + parallelSet.ID = bson.NewObjectId() + parallelSet.Name = "test" + err := parallelsetDao.Insert(parallelSet) + if err != nil { + t.Fail() + } + parallelSets, err = parallelsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(parallelSets) != 1 { + t.Fail() + } +} + +func TestUpsertParallelSet(t *testing.T) { + var ( + parallelsetDao = ParallelSetDAO{Db: dao.TestDb} + parallelSet = ParallelSet{} + ) + parallelSet.ID = bson.NewObjectId() + parallelSet.Name = "test2" + parallelsetDao.Upsert(parallelSet) + parallelSets, err := parallelsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(parallelSets) != 2 { + t.Fail() + } +} + +func TestDeleteParallelSet(t *testing.T) { + var ( + parallelsetDao = ParallelSetDAO{Db: dao.TestDb} + ) + parallelSets, err := parallelsetDao.FindAll() + if err != nil { + t.Fail() + } + for _, parallelSet := range parallelSets { + parallelsetDao.Delete(parallelSet) + } + parallelSets, err = parallelsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(parallelSets) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/player.go b/go/pkg/tysc/player.go new file mode 100644 index 0000000..055efe6 --- /dev/null +++ b/go/pkg/tysc/player.go @@ -0,0 +1,13 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Player defines the data model for TYSC players with id, first and lastname. +type Player struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Firstname string `json:"firstname" bson:"firstname"` + Lastname string `json:"lastname" bson:"lastname"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/player_dao.go b/go/pkg/tysc/player_dao.go new file mode 100644 index 0000000..ff60c37 --- /dev/null +++ b/go/pkg/tysc/player_dao.go @@ -0,0 +1,74 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// PlayerDAO extends the type BaseDAO. +type PlayerDAO struct { + Db dao.BaseDAO +} + +const ( + // PLAYERCOLLECTION defines the collection name for storing players. + PLAYERCOLLECTION = "player" + // PLAYERMODEL defines the name of the player data model. + PLAYERMODEL = "kontor.tysc.player" +) + +// FindAll retrieves the list of players from the database. +func (m *PlayerDAO) FindAll() ([]Player, error) { + m.Db.Connect() + var players []Player + err := m.Db.MongoDb.C(PLAYERCOLLECTION).Find(bson.M{"model": PLAYERMODEL}).All(&players) + return players, err +} + +// FindByID returns a player with given id or returns the error. +func (m *PlayerDAO) FindByID(id string) (Player, error) { + m.Db.Connect() + var player Player + err := m.Db.MongoDb.C(PLAYERCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&player) + return player, err +} + +// FindByLastName returns a player with given last name or returns the error. +func (m *PlayerDAO) FindByLastName(lastname string) (Player, error) { + m.Db.Connect() + var player Player + err := m.Db.MongoDb.C(PLAYERCOLLECTION).Find(bson.M{"lastname": lastname, "model": PLAYERMODEL}).One(&player) + return player, err +} + +// Insert a player into database. +func (m *PlayerDAO) Insert(player Player) error { + m.Db.Connect() + player.Model = PLAYERMODEL + err := m.Db.MongoDb.C(PLAYERCOLLECTION).Insert(&player) + return err +} + +// Upsert a player into database. +func (m *PlayerDAO) Upsert(player Player) (*mgo.ChangeInfo, error) { + m.Db.Connect() + player.Model = PLAYERMODEL + info, err := m.Db.MongoDb.C(PLAYERCOLLECTION).Upsert(bson.M{"lastname": player.Lastname}, &player) + return info, err +} + +// Update an existing player. +func (m *PlayerDAO) Update(player Player) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PLAYERCOLLECTION).UpdateId(player.ID, &player) + return err +} + +// Delete an existing player. +func (m *PlayerDAO) Delete(player Player) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PLAYERCOLLECTION).Remove(&player) + return err +} diff --git a/go/pkg/tysc/player_test.go b/go/pkg/tysc/player_test.go new file mode 100644 index 0000000..e839ee9 --- /dev/null +++ b/go/pkg/tysc/player_test.go @@ -0,0 +1,103 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var playerModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Firstname", "string"}, + {"Lastname", "string"}, + {"Model", "string"}, +} + +func TestPlayerModel(t *testing.T) { + m := Player{} + if reflect.TypeOf(m).NumField() != len(playerModelTestTable) { + t.Fail() + } + for index, testData := range playerModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListPlayers(t *testing.T) { + var ( + playerDao = PlayerDAO{Db: dao.TestDb} + ) + players, err := playerDao.FindAll() + if err != nil { + t.Fail() + } + if players != nil { + t.Fail() + } +} + +func TestInsertPlayer(t *testing.T) { + var ( + playerDao = PlayerDAO{Db: dao.TestDb} + player = Player{} + players []Player + ) + player.ID = bson.NewObjectId() + player.Lastname = "test" + err := playerDao.Insert(player) + if err != nil { + t.Fail() + } + players, err = playerDao.FindAll() + if err != nil { + t.Fail() + } + if len(players) != 1 { + t.Fail() + } +} + +func TestUpsertPlayer(t *testing.T) { + var ( + playerDao = PlayerDAO{Db: dao.TestDb} + player = Player{} + ) + player.ID = bson.NewObjectId() + player.Lastname = "test2" + playerDao.Upsert(player) + players, err := playerDao.FindAll() + if err != nil { + t.Fail() + } + if len(players) != 2 { + t.Fail() + } +} + +func TestDeletePlayer(t *testing.T) { + var ( + playerDao = PlayerDAO{Db: dao.TestDb} + ) + players, err := playerDao.FindAll() + if err != nil { + t.Fail() + } + for _, player := range players { + playerDao.Delete(player) + } + players, err = playerDao.FindAll() + if err != nil { + t.Fail() + } + if len(players) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/position.go b/go/pkg/tysc/position.go new file mode 100644 index 0000000..b52703a --- /dev/null +++ b/go/pkg/tysc/position.go @@ -0,0 +1,14 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Position defines the data model for TYSC positions with id, name, description and sport. +type Position struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Description string `json:"description" bson:"description"` + Sport bson.ObjectId `json:"sport" bson:"sport,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/position_dao.go b/go/pkg/tysc/position_dao.go new file mode 100644 index 0000000..4166ee0 --- /dev/null +++ b/go/pkg/tysc/position_dao.go @@ -0,0 +1,74 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// PositionDAO extends the type BaseDAO. +type PositionDAO struct { + Db dao.BaseDAO +} + +const ( + // POSITIONCOLLECTION defines the collection name for storing positions. + POSITIONCOLLECTION = "position" + // POSITIONMODEL defines the name of the position data model. + POSITIONMODEL = "kontor.tysc.position" +) + +// FindAll retrieves the list of positions from the database. +func (m *PositionDAO) FindAll() ([]Position, error) { + m.Db.Connect() + var positions []Position + err := m.Db.MongoDb.C(POSITIONCOLLECTION).Find(bson.M{"model": POSITIONMODEL}).All(&positions) + return positions, err +} + +// FindByID returns a position with given id or returns the error. +func (m *PositionDAO) FindByID(id string) (Position, error) { + m.Db.Connect() + var position Position + err := m.Db.MongoDb.C(POSITIONCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&position) + return position, err +} + +// FindByName returns a position with given name or returns the error. +func (m *PositionDAO) FindByName(name string) (Position, error) { + m.Db.Connect() + var position Position + err := m.Db.MongoDb.C(POSITIONCOLLECTION).Find(bson.M{"name": name, "model": POSITIONMODEL}).One(&position) + return position, err +} + +// Insert a position into database. +func (m *PositionDAO) Insert(position Position) error { + m.Db.Connect() + position.Model = POSITIONMODEL + err := m.Db.MongoDb.C(POSITIONCOLLECTION).Insert(&position) + return err +} + +// Upsert a position into database. +func (m *PositionDAO) Upsert(position Position) (*mgo.ChangeInfo, error) { + m.Db.Connect() + position.Model = POSITIONMODEL + info, err := m.Db.MongoDb.C(POSITIONCOLLECTION).Upsert(bson.M{"name": position.Name}, &position) + return info, err +} + +// Update an existing position. +func (m *PositionDAO) Update(position Position) error { + m.Db.Connect() + err := m.Db.MongoDb.C(POSITIONCOLLECTION).UpdateId(position.ID, &position) + return err +} + +// Delete an existing position. +func (m *PositionDAO) Delete(position Position) error { + m.Db.Connect() + err := m.Db.MongoDb.C(POSITIONCOLLECTION).Remove(&position) + return err +} diff --git a/go/pkg/tysc/position_test.go b/go/pkg/tysc/position_test.go new file mode 100644 index 0000000..2b97105 --- /dev/null +++ b/go/pkg/tysc/position_test.go @@ -0,0 +1,104 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var positionModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Description", "string"}, + {"Sport", "string"}, + {"Model", "string"}, +} + +func TestPositionModel(t *testing.T) { + m := Position{} + if reflect.TypeOf(m).NumField() != len(positionModelTestTable) { + t.Fail() + } + for index, testData := range positionModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListPositions(t *testing.T) { + var ( + positionDao = PositionDAO{Db: dao.TestDb} + ) + positions, err := positionDao.FindAll() + if err != nil { + t.Fail() + } + if len(positions) != 0 { + t.Fail() + } +} + +func TestInsertPosition(t *testing.T) { + var ( + positionDao = PositionDAO{Db: dao.TestDb} + position = Position{} + positions []Position + ) + position.ID = bson.NewObjectId() + position.Name = "test" + err := positionDao.Insert(position) + if err != nil { + t.Fail() + } + positions, err = positionDao.FindAll() + if err != nil { + t.Fail() + } + if len(positions) != 1 { + t.Fail() + } +} + +func TestUpsertPosition(t *testing.T) { + var ( + positionDao = PositionDAO{Db: dao.TestDb} + position = Position{} + ) + position.ID = bson.NewObjectId() + position.Name = "test2" + positionDao.Upsert(position) + positions, err := positionDao.FindAll() + if err != nil { + t.Fail() + } + if len(positions) != 2 { + t.Fail() + } +} + +func TestDeletePosition(t *testing.T) { + var ( + positionDao = PositionDAO{Db: dao.TestDb} + ) + positions, err := positionDao.FindAll() + if err != nil { + t.Fail() + } + for _, position := range positions { + positionDao.Delete(position) + } + positions, err = positionDao.FindAll() + if err != nil { + t.Fail() + } + if len(positions) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/routes.go b/go/pkg/tysc/routes.go new file mode 100644 index 0000000..e581db7 --- /dev/null +++ b/go/pkg/tysc/routes.go @@ -0,0 +1,22 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + + "github.com/gin-gonic/gin" +) + +// GetRoutes returns all routes for TYSC related data. +func GetRoutes(router *gin.Engine) { + tyscRoutes := router.Group("/tysc") + tyscRoutes.GET("/", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/sport", auth.EnsureLoggedIn(), showSportList) + tyscRoutes.GET("/position", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/team", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/player", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/manufacturer", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/cardset", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/parallelset", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/insertset", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/card", auth.EnsureLoggedIn(), showIndexPage) +} diff --git a/go/pkg/tysc/sport.go b/go/pkg/tysc/sport.go new file mode 100644 index 0000000..ed5b8ef --- /dev/null +++ b/go/pkg/tysc/sport.go @@ -0,0 +1,12 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Sport defines the data model for TYSC sports with id, and name. +type Sport struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/sport_dao.go b/go/pkg/tysc/sport_dao.go new file mode 100644 index 0000000..e931f98 --- /dev/null +++ b/go/pkg/tysc/sport_dao.go @@ -0,0 +1,74 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// SportDAO extends the type BaseDAO. +type SportDAO struct { + Db dao.BaseDAO +} + +const ( + // SPORTCOLLECTION defines the collection name for storing sports. + SPORTCOLLECTION = "sport" + // SPORTMODEL defines the name of the sport data model. + SPORTMODEL = "kontor.tysc.sport" +) + +// FindAll retrieves the list of sports from the database. +func (m *SportDAO) FindAll() ([]Sport, error) { + m.Db.Connect() + var sports []Sport + err := m.Db.MongoDb.C(SPORTCOLLECTION).Find(bson.M{"model": SPORTMODEL}).All(&sports) + return sports, err +} + +// FindByID returns a sport with given id or returns the error. +func (m *SportDAO) FindByID(id string) (Sport, error) { + m.Db.Connect() + var sport Sport + err := m.Db.MongoDb.C(SPORTCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&sport) + return sport, err +} + +// FindByName returns a sport with given name or returns the error. +func (m *SportDAO) FindByName(name string) (Sport, error) { + m.Db.Connect() + var sport Sport + err := m.Db.MongoDb.C(SPORTCOLLECTION).Find(bson.M{"name": name, "model": SPORTMODEL}).One(&sport) + return sport, err +} + +// Insert a sport into database. +func (m *SportDAO) Insert(sport Sport) error { + m.Db.Connect() + sport.Model = SPORTMODEL + err := m.Db.MongoDb.C(SPORTCOLLECTION).Insert(&sport) + return err +} + +// Upsert a sport into database. +func (m *SportDAO) Upsert(sport Sport) (*mgo.ChangeInfo, error) { + m.Db.Connect() + sport.Model = SPORTMODEL + info, err := m.Db.MongoDb.C(SPORTCOLLECTION).Upsert(bson.M{"name": sport.Name}, &sport) + return info, err +} + +// Update an existing sport. +func (m *SportDAO) Update(sport Sport) error { + m.Db.Connect() + err := m.Db.MongoDb.C(SPORTCOLLECTION).UpdateId(sport.ID, &sport) + return err +} + +// Delete an existing sport. +func (m *SportDAO) Delete(sport Sport) error { + m.Db.Connect() + err := m.Db.MongoDb.C(SPORTCOLLECTION).Remove(&sport) + return err +} diff --git a/go/pkg/tysc/sport_test.go b/go/pkg/tysc/sport_test.go new file mode 100644 index 0000000..0aaf1c1 --- /dev/null +++ b/go/pkg/tysc/sport_test.go @@ -0,0 +1,102 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var sportModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Model", "string"}, +} + +func TestSport(t *testing.T) { + m := Sport{} + if reflect.TypeOf(m).NumField() != len(sportModelTestTable) { + t.Fail() + } + for index, testData := range sportModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListSports(t *testing.T) { + var ( + sportDao = SportDAO{Db: dao.TestDb} + ) + sports, err := sportDao.FindAll() + if err != nil { + t.Fail() + } + if len(sports) != 0 { + t.Fail() + } +} + +func TestInsertSport(t *testing.T) { + var ( + sportDao = SportDAO{Db: dao.TestDb} + sport = Sport{} + sports []Sport + ) + sport.ID = bson.NewObjectId() + sport.Name = "test" + err := sportDao.Insert(sport) + if err != nil { + t.Fail() + } + sports, err = sportDao.FindAll() + if err != nil { + t.Fail() + } + if len(sports) != 1 { + t.Fail() + } +} + +func TestUpsertSport(t *testing.T) { + var ( + sportDao = SportDAO{Db: dao.TestDb} + sport = Sport{} + ) + sport.ID = bson.NewObjectId() + sport.Name = "test2" + sportDao.Upsert(sport) + sports, err := sportDao.FindAll() + if err != nil { + t.Fail() + } + if len(sports) != 2 { + t.Fail() + } +} + +func TestDeleteSport(t *testing.T) { + var ( + sportDao = SportDAO{Db: dao.TestDb} + ) + sports, err := sportDao.FindAll() + if err != nil { + t.Fail() + } + for _, sport := range sports { + sportDao.Delete(sport) + } + sports, err = sportDao.FindAll() + if err != nil { + t.Fail() + } + if len(sports) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/team.go b/go/pkg/tysc/team.go new file mode 100644 index 0000000..167a2e5 --- /dev/null +++ b/go/pkg/tysc/team.go @@ -0,0 +1,14 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Team defines the data model for TYSC teams with id, name and sport. +type Team struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Shortname string `json:"shortname" bson:"shortname"` + Sport bson.ObjectId `json:"sport" bson:"sport,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/team_dao.go b/go/pkg/tysc/team_dao.go new file mode 100644 index 0000000..911763e --- /dev/null +++ b/go/pkg/tysc/team_dao.go @@ -0,0 +1,74 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// TeamDAO extends the type BaseDAO. +type TeamDAO struct { + Db dao.BaseDAO +} + +const ( + // TEAMCOLLECTION defines the collection name for storing teams. + TEAMCOLLECTION = "team" + // TEAMMODEL defines the name of the team data model. + TEAMMODEL = "kontor.tysc.team" +) + +// FindAll retrieves the list of cards from the database. +func (m *TeamDAO) FindAll() ([]Team, error) { + m.Db.Connect() + var teams []Team + err := m.Db.MongoDb.C(TEAMCOLLECTION).Find(bson.M{"model": TEAMMODEL}).All(&teams) + return teams, err +} + +// FindByID returns a team with given id or returns the error. +func (m *TeamDAO) FindByID(id string) (Team, error) { + m.Db.Connect() + var team Team + err := m.Db.MongoDb.C(TEAMCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&team) + return team, err +} + +// FindByName returns a team with given name or returns the error. +func (m *TeamDAO) FindByName(name string) (Team, error) { + m.Db.Connect() + var team Team + err := m.Db.MongoDb.C(TEAMCOLLECTION).Find(bson.M{"name": name, "model": TEAMMODEL}).One(&team) + return team, err +} + +// Insert a team into database +func (m *TeamDAO) Insert(team Team) error { + m.Db.Connect() + team.Model = TEAMMODEL + err := m.Db.MongoDb.C(TEAMCOLLECTION).Insert(&team) + return err +} + +// Upsert a team into database +func (m *TeamDAO) Upsert(team Team) (*mgo.ChangeInfo, error) { + m.Db.Connect() + team.Model = TEAMMODEL + info, err := m.Db.MongoDb.C(TEAMCOLLECTION).Upsert(bson.M{"name": team.Name}, &team) + return info, err +} + +// Update an existing team. +func (m *TeamDAO) Update(team Team) error { + m.Db.Connect() + err := m.Db.MongoDb.C(TEAMCOLLECTION).UpdateId(team.ID, &team) + return err +} + +// Delete an existing team. +func (m *TeamDAO) Delete(team Team) error { + m.Db.Connect() + err := m.Db.MongoDb.C(TEAMCOLLECTION).Remove(&team) + return err +} diff --git a/go/pkg/tysc/team_test.go b/go/pkg/tysc/team_test.go new file mode 100644 index 0000000..79dd539 --- /dev/null +++ b/go/pkg/tysc/team_test.go @@ -0,0 +1,103 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var teamModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Shortname", "string"}, + {"Sport", "string"}, + {"Model", "string"}, +} + +func TestTeamModel(t *testing.T) { + m := Team{} + if reflect.TypeOf(m).NumField() != len(teamModelTestTable) { + t.Fail() + } + for index, testData := range teamModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListTeams(t *testing.T) { + var ( + teamDao = TeamDAO{Db: dao.TestDb} + ) + teams, err := teamDao.FindAll() + if err != nil { + t.Fail() + } + if teams != nil { + t.Fail() + } +} + +func TestInsertTeam(t *testing.T) { + var ( + teamDao = TeamDAO{Db: dao.TestDb} + team = Team{} + teams []Team + ) + team.ID = bson.NewObjectId() + err := teamDao.Insert(team) + if err != nil { + t.Fail() + } + teams, err = teamDao.FindAll() + if err != nil { + t.Fail() + } + if len(teams) != 1 { + t.Fail() + } +} + +func TestUpsertTeam(t *testing.T) { + var ( + teamDao = TeamDAO{Db: dao.TestDb} + team = Team{} + ) + team.ID = bson.NewObjectId() + team.Name = "test2" + teamDao.Upsert(team) + teams, err := teamDao.FindAll() + if err != nil { + t.Fail() + } + if len(teams) != 2 { + t.Fail() + } +} + +func TestDeleteTeam(t *testing.T) { + var ( + teamDao = TeamDAO{Db: dao.TestDb} + ) + teams, err := teamDao.FindAll() + if err != nil { + t.Fail() + } + for _, team := range teams { + teamDao.Delete(team) + } + teams, err = teamDao.FindAll() + if err != nil { + t.Fail() + } + if len(teams) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/views.go b/go/pkg/tysc/views.go new file mode 100644 index 0000000..0d9f233 --- /dev/null +++ b/go/pkg/tysc/views.go @@ -0,0 +1,22 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + "net/http" + + "github.com/gin-gonic/gin" +) + +func showIndexPage(c *gin.Context) { + util.Render(c, gin.H{"title": "TradeYourSportsCards", "payload": nil}, "tysc/index.html") +} + +func showSportList(c *gin.Context) { + var dao = SportDAO{Db: dao.KontorDb} + if sports, err := dao.FindAll(); err == nil { + util.Render(c, gin.H{"title": "TYSC Sport List", "payload": sports}, "tysc/sports.html") + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} diff --git a/go/pkg/util/render.go b/go/pkg/util/render.go new file mode 100644 index 0000000..b786b4c --- /dev/null +++ b/go/pkg/util/render.go @@ -0,0 +1,39 @@ +package util + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + "net/http" + + "github.com/gin-gonic/gin" +) + +const ( + // SaveAction defines label of button. + SaveAction = "Save" + // AddAction defines label of button. + AddAction = "Add" + // DeleteAction defines label of button. + DeleteAction = "Delete" +) + +// Render one of HTML, JSON or CSV based on the 'Accept' header of the request +// If the header doesn't specify this, HTML is rendered, provided that +// the template name is present +func Render(c *gin.Context, data gin.H, templateName string) { + auth.SetSessionData(c, data) + switch c.Request.Header.Get("Accept") { + case "application/json": + c.JSON(http.StatusOK, data["payload"]) + case "application/xml": + c.XML(http.StatusOK, data["payload"]) + default: + c.HTML(http.StatusOK, templateName, data) + } +} + +// ShowIndexPage render the index page of Kontor web application. +func ShowIndexPage(c *gin.Context) { + // Call the render function with the name of the template to render + //log.Printf("Context: %v", c) + Render(c, gin.H{"title": "Kontor", "payload": nil}, "kontor/index.html") +} diff --git a/go/sonar-project.properties b/go/sonar-project.properties new file mode 100644 index 0000000..b06f378 --- /dev/null +++ b/go/sonar-project.properties @@ -0,0 +1,2 @@ +sonar.projectKey=kontor_kontor-go_AX-cQT62rXuu6JVRvr-z +sonar.qualitygate.wait=true diff --git a/go/templates/comics/artists.html b/go/templates/comics/artists.html new file mode 100644 index 0000000..f49ebbc --- /dev/null +++ b/go/templates/comics/artists.html @@ -0,0 +1,24 @@ + +{{ template "header.html" .}} +{{ template "comics/menu.html" .}} + + +
+ + + + {{range .payload }} + + {{end}} +
Comic Artists
Name
{{.Name}}
+
+ Add entry +
+
+ + +{{ template "footer.html" .}} diff --git a/go/templates/comics/comic.html b/go/templates/comics/comic.html new file mode 100644 index 0000000..8184605 --- /dev/null +++ b/go/templates/comics/comic.html @@ -0,0 +1,34 @@ +{{ template "header.html" .}} +{{ template "comics/menu.html" .}} + +
+ {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} +
+
+ + {{ if .payload.Title }} + + {{ else }} + + {{ end }} +
+
+ + +
+ + + {{ if eq .action "Save" }} + + {{ end }} +
+
+{{ template "footer.html" .}} diff --git a/go/templates/comics/comics.html b/go/templates/comics/comics.html new file mode 100644 index 0000000..aeca37f --- /dev/null +++ b/go/templates/comics/comics.html @@ -0,0 +1,24 @@ + +{{ template "header.html" .}} +{{ template "comics/menu.html" .}} + + +
+ + + + {{range .payload }} + + {{end}} +
List of Comics
Name
{{.Title}}
+
+ Add entry +
+
+ + +{{ template "footer.html" .}} diff --git a/go/templates/comics/menu.html b/go/templates/comics/menu.html new file mode 100644 index 0000000..6fec416 --- /dev/null +++ b/go/templates/comics/menu.html @@ -0,0 +1,38 @@ +{{ define "comics/menu.html" }} + +{{ end }} diff --git a/go/templates/comics/publisher.html b/go/templates/comics/publisher.html new file mode 100644 index 0000000..fe38dcd --- /dev/null +++ b/go/templates/comics/publisher.html @@ -0,0 +1,32 @@ +{{ define "comics/publisher.html" }} +{{ template "header.html" .}} +{{ template "comics/menu.html" .}} + +
+ {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} +
+
+ + {{ if .payload.Name }} + + {{ else }} + + {{ end }} +
+ + + {{ if eq .action "Save" }} + + {{ end }} +
+
+{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/comics/publishers.html b/go/templates/comics/publishers.html new file mode 100644 index 0000000..d691cdc --- /dev/null +++ b/go/templates/comics/publishers.html @@ -0,0 +1,25 @@ + +{{ define "comics/publishers.html" }} +{{ template "header.html" .}} +{{ template "comics/menu.html" .}} + + +
+ + + + {{range .payload }} + + {{end}} +
Comic Publishers
Name
{{.Name}}
+
+ Add entry +
+
+ +{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/kontor/admin-menu.html b/go/templates/kontor/admin-menu.html new file mode 100644 index 0000000..d5738a2 --- /dev/null +++ b/go/templates/kontor/admin-menu.html @@ -0,0 +1,34 @@ + diff --git a/go/templates/kontor/admin.html b/go/templates/kontor/admin.html new file mode 100644 index 0000000..9dd19ad --- /dev/null +++ b/go/templates/kontor/admin.html @@ -0,0 +1,11 @@ + +{{ define "kontor/admin.html" }} +{{ template "header.html" .}} +{{ template "admin-menu.html" .}} + + +{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/kontor/create-user.html b/go/templates/kontor/create-user.html new file mode 100644 index 0000000..1a67785 --- /dev/null +++ b/go/templates/kontor/create-user.html @@ -0,0 +1,40 @@ +{{ define "kontor/create-user.html" }} +{{ template "header.html" .}} +{{ template "admin-menu.html" .}} + + +
+
+ + {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/kontor/data-upload.html b/go/templates/kontor/data-upload.html new file mode 100644 index 0000000..b2f77f2 --- /dev/null +++ b/go/templates/kontor/data-upload.html @@ -0,0 +1,29 @@ +{{ define "kontor/data-upload.html" }} +{{ template "header.html" .}} +{{ template "admin-menu.html" .}} + + +
+
+ + {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} + +
+
+ + +
+ +
+
+
+ +{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/kontor/footer.html b/go/templates/kontor/footer.html new file mode 100644 index 0000000..3760315 --- /dev/null +++ b/go/templates/kontor/footer.html @@ -0,0 +1,26 @@ + + + + + + + diff --git a/go/templates/kontor/header.html b/go/templates/kontor/header.html new file mode 100644 index 0000000..bcf20aa --- /dev/null +++ b/go/templates/kontor/header.html @@ -0,0 +1,17 @@ + + + + + + + {{ .title }} + + + + + + + + + + diff --git a/go/templates/kontor/index.html b/go/templates/kontor/index.html new file mode 100644 index 0000000..4e23ba7 --- /dev/null +++ b/go/templates/kontor/index.html @@ -0,0 +1,17 @@ + +{{ define "kontor/index.html" }} + +{{ template "header.html" .}} + +{{ template "menu.html" . }} + +
+ {{ if .InfoMessage}} +

{{.InfoMessage}}

+ {{end}} +
+ + +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/kontor/login-successful.html b/go/templates/kontor/login-successful.html new file mode 100644 index 0000000..46e07fa --- /dev/null +++ b/go/templates/kontor/login-successful.html @@ -0,0 +1,12 @@ + + + +{{ template "header.html" .}} +{{ template "menu.html" . }} + +
+ You have successfully logged in. +
+ + +{{ template "footer.html" .}} diff --git a/go/templates/kontor/login.html b/go/templates/kontor/login.html new file mode 100644 index 0000000..83fce69 --- /dev/null +++ b/go/templates/kontor/login.html @@ -0,0 +1,35 @@ + + + +{{ template "header.html" .}} +{{ template "menu.html" . }} + +

Login

+ + +
+
+ + {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} + +
+
+ + +
+
+ + +
+ +
+
+
+ + + +{{ template "footer.html" .}} diff --git a/go/templates/kontor/menu.html b/go/templates/kontor/menu.html new file mode 100644 index 0000000..b52a358 --- /dev/null +++ b/go/templates/kontor/menu.html @@ -0,0 +1,36 @@ + diff --git a/go/templates/kontor/user-detail.html b/go/templates/kontor/user-detail.html new file mode 100644 index 0000000..983a614 --- /dev/null +++ b/go/templates/kontor/user-detail.html @@ -0,0 +1,59 @@ +{{ define "kontor/user-detail.html" }} +{{ template "header.html" .}} +{{ template "admin-menu.html" .}} + + +
+ + {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} +
+
+ + {{ if .payload.Username }} + + {{ else }} + + {{ end }} +
+
+
+ + {{ if .payload.Firstname }} + + {{ else }} + + {{ end }} +
+
+ + {{ if .payload.Lastname }} + + {{ else }} + + {{ end }} +
+
+
+ + +
+
+ + +
+ + + {{ if eq .action "Save" }} + + {{ end }} +
+
+{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/kontor/users.html b/go/templates/kontor/users.html new file mode 100644 index 0000000..8aef51c --- /dev/null +++ b/go/templates/kontor/users.html @@ -0,0 +1,38 @@ +{{ define "kontor/users.html" }} +{{ template "header.html" .}} +{{ template "admin-menu.html" .}} + + +
+ + + + + + + + + {{range .payload }} + + + + + {{ if .IsAdmin}} + + {{end}} + {{ if not .IsAdmin }} + + {{end}} + + {{end}} +
Registered users of Kontor
UsernameFirst NameLast NameAdministrator
{{.Username}}{{.Firstname}}{{.Lastname}}
+
+ Add entry +
+
+ +{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/library/authors.html b/go/templates/library/authors.html new file mode 100644 index 0000000..781ec26 --- /dev/null +++ b/go/templates/library/authors.html @@ -0,0 +1,25 @@ + +{{ define "library/authors.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" .}} + +
+ + + + {{range .payload }} + + {{end}} +
Liste der Autoren
Name
{{.Name}}
+
+ Add entry +
+
+ + +{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/library/books.html b/go/templates/library/books.html new file mode 100644 index 0000000..456cb82 --- /dev/null +++ b/go/templates/library/books.html @@ -0,0 +1,25 @@ + +{{ define "library/books.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" . }} + +
+ + + + {{range .payload }} + + {{end}} +
Liste der Bücher
Title
{{.Title}}
+
+ Add entry +
+
+ +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/library/menu.html b/go/templates/library/menu.html new file mode 100644 index 0000000..2e6257f --- /dev/null +++ b/go/templates/library/menu.html @@ -0,0 +1,38 @@ +{{ define "library/menu.html" }} + +{{ end }} diff --git a/go/templates/library/publisher.html b/go/templates/library/publisher.html new file mode 100644 index 0000000..c0dae5e --- /dev/null +++ b/go/templates/library/publisher.html @@ -0,0 +1,32 @@ +{{ define "library/publisher.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" .}} + +
+ {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} +
+
+ + {{ if .payload.Name }} + + {{ else }} + + {{ end }} +
+ + + {{ if eq .action "Save" }} + + {{ end }} +
+
+{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/library/publishers.html b/go/templates/library/publishers.html new file mode 100644 index 0000000..a166167 --- /dev/null +++ b/go/templates/library/publishers.html @@ -0,0 +1,24 @@ + +{{ define "library/publishers.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" .}} + +
+ + + + {{range .payload }} + + {{end}} +
Liste der Verlage
Name
{{.Name}}
+
+ Add entry +
+
+ +{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/office/index.html b/go/templates/office/index.html new file mode 100644 index 0000000..3932a29 --- /dev/null +++ b/go/templates/office/index.html @@ -0,0 +1,10 @@ + +{{ define "office/index.html" }} +{{ template "header.html" .}} +{{ template "office/menu.html" . }} + +

Home Office

+ +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/office/menu.html b/go/templates/office/menu.html new file mode 100644 index 0000000..ddcd03d --- /dev/null +++ b/go/templates/office/menu.html @@ -0,0 +1,38 @@ +{{ define "office/menu.html" }} + +{{ end }} diff --git a/go/templates/tradingcards/index.html b/go/templates/tradingcards/index.html new file mode 100644 index 0000000..6911e17 --- /dev/null +++ b/go/templates/tradingcards/index.html @@ -0,0 +1,10 @@ + +{{ define "tradingcards/index.html" }} +{{ template "header.html" .}} +{{ template "tradingcards/menu.html" . }} + +

Trading Cards

+ +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/tradingcards/menu.html b/go/templates/tradingcards/menu.html new file mode 100644 index 0000000..524080c --- /dev/null +++ b/go/templates/tradingcards/menu.html @@ -0,0 +1,38 @@ +{{ define "tradingcards/menu.html" }} + +{{ end }} diff --git a/go/templates/tysc/index.html b/go/templates/tysc/index.html new file mode 100644 index 0000000..b1d135d --- /dev/null +++ b/go/templates/tysc/index.html @@ -0,0 +1,16 @@ +{{ define "tysc/index.html" }} +{{ template "header.html" .}} +{{ template "tysc/menu.html" . }} + +{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/tysc/menu.html b/go/templates/tysc/menu.html new file mode 100644 index 0000000..53a8e9e --- /dev/null +++ b/go/templates/tysc/menu.html @@ -0,0 +1,38 @@ +{{ define "tysc/menu.html" }} + +{{ end }} diff --git a/go/templates/tysc/sports.html b/go/templates/tysc/sports.html new file mode 100644 index 0000000..cf65f25 --- /dev/null +++ b/go/templates/tysc/sports.html @@ -0,0 +1,28 @@ +{{ define "tysc/sports.html" }} +{{ template "header.html" .}} +{{ template "tysc/menu.html" .}} + +
+ + + + {{range .payload }} + + {{end}} +
List of Amertican Sports
Name
{{.Name}}
+
+ Add entry +
+
+{{ template "footer.html" .}} +{{ end }} diff --git a/go/tysc-20041010-1819.sql b/go/tysc-20041010-1819.sql new file mode 100644 index 0000000..167c41d --- /dev/null +++ b/go/tysc-20041010-1819.sql @@ -0,0 +1,168 @@ +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=NO_AUTO_VALUE_ON_ZERO */; +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `tysc`; +USE `tysc`; + +DROP TABLE IF EXISTS `angebote`; +CREATE TABLE `angebote` ( + `user_id` int(11) NOT NULL default '0', + `karte_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; +INSERT INTO `angebote` (`user_id`,`karte_id`) VALUES (3,28),(3,30); + +DROP TABLE IF EXISTS `benutzer`; +CREATE TABLE `benutzer` ( + `ID` int(11) NOT NULL auto_increment, + `forename` varchar(40) default NULL, + `surname` varchar(40) default NULL, + `strasse` varchar(60) default NULL, + `plz` int(6) default NULL, + `ort` varchar(20) default NULL, + `username` varchar(20) default NULL, + `email` varchar(60) default NULL, + `password` varchar(32) default NULL, + `java` char(1) default NULL, + `language` int(11) NOT NULL default '0', + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `benutzer` (`ID`,`forename`,`surname`,`strasse`,`plz`,`ort`,`username`,`email`,`password`,`java`,`language`) VALUES (1,'Thomas','Peetz','Reichweindamm 24',13627,'Berlin','gophard','thomas.peetz@snafu.de','t.log1n','N',1),(2,'Heiko','John','Johannastr.49',13581,'Berlin','John','healjo@hotmail.com','redskins','N',1),(3,'Thomas','Peetz','Reichweindamm 24',13627,'Berlin','peetz','gophard@snafu.de','peetz','N',1); + +DROP TABLE IF EXISTS `changelog`; +CREATE TABLE `changelog` ( + `datum` date default NULL, + `tablename` varchar(20) default NULL, + `id` int(11) NOT NULL default '0' +) TYPE=MyISAM; +INSERT INTO `changelog` (`datum`,`tablename`,`id`) VALUES ('2002-02-27','benutzer',1),('2002-02-28','benutzer',2),('2002-03-05','benutzer',3),('2002-03-05','angebote',0),('2002-03-05','angebote',0); + +DROP TABLE IF EXISTS `hersteller`; +CREATE TABLE `hersteller` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(30) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `hersteller` (`ID`,`name`) VALUES (1,'Pacific'),(2,'Fleer'),(3,'Bowman'),(6,'Topps'),(7,'Donruss'),(8,'Score'),(9,'Flair'); + +DROP TABLE IF EXISTS `inserts`; +CREATE TABLE `inserts` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `inserts` (`ID`,`hersteller_id`,`name`) VALUES (1,2,'Mystique Big Buzz'); + +DROP TABLE IF EXISTS `karte`; +CREATE TABLE `karte` ( + `ID` int(11) NOT NULL auto_increment, + `spieler_id` int(11) NOT NULL default '0', + `team_id` int(11) NOT NULL default '0', + `hersteller_id` int(11) NOT NULL default '0', + `serie_id` int(11) default NULL, + `parallel_id` int(11) default NULL, + `inserts_id` int(11) default NULL, + `rookie` char(1) default NULL, + `jahr` int(4) default NULL, + `nummer` int(11) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `karte` (`ID`,`spieler_id`,`team_id`,`hersteller_id`,`serie_id`,`parallel_id`,`inserts_id`,`rookie`,`jahr`,`nummer`) VALUES (12,12,13,1,1,0,0,'N',2001,212),(1,1,2,1,1,0,0,'N',2001,185),(2,2,2,1,1,0,0,'N',2001,250),(3,3,8,1,1,0,0,'N',2001,103),(4,4,8,1,1,0,0,'N',2001,112),(5,5,6,1,1,0,0,'N',2001,37),(6,6,6,1,1,0,0,'N',2001,38),(7,7,6,1,1,0,0,'N',2001,31),(8,8,10,1,1,0,0,'N',2001,338),(9,9,10,1,1,0,0,'N',2001,335),(10,10,10,1,1,0,0,'N',2001,345),(11,11,13,1,1,0,0,'N',2001,213),(13,13,14,1,1,0,0,'N',2001,311),(14,14,14,1,1,0,0,'N',2001,312),(15,15,16,1,1,0,0,'N',2001,403),(16,16,16,1,1,0,0,'N',2001,397),(17,17,16,1,1,0,0,'N',2001,404),(18,18,18,1,1,0,0,'N',2001,116),(19,19,18,1,1,0,0,'N',2001,122),(20,20,18,1,1,0,0,'N',2001,117),(21,21,19,1,1,0,0,'N',2001,281),(22,22,19,1,1,0,0,'N',2001,321),(23,23,20,1,1,0,0,'N',2001,331),(24,24,20,1,1,0,0,'N',2001,324),(25,25,21,1,1,0,0,'N',2001,445),(26,26,27,1,1,0,0,'N',2001,28),(27,27,27,1,1,0,0,'N',2001,17),(28,28,27,1,1,0,0,'N',2001,23),(29,29,29,1,1,0,0,'N',2001,273); +INSERT INTO `karte` (`ID`,`spieler_id`,`team_id`,`hersteller_id`,`serie_id`,`parallel_id`,`inserts_id`,`rookie`,`jahr`,`nummer`) VALUES (30,30,31,1,1,0,0,'N',2001,380),(31,31,31,1,1,0,0,'N',2001,390),(32,32,31,1,1,0,0,'N',2001,381),(33,33,31,1,1,0,0,'N',2001,387),(34,34,31,1,1,0,0,'N',2001,386),(35,35,30,1,1,0,0,'N',2001,349),(36,36,30,1,1,0,0,'N',2001,350),(37,37,44,5,8,0,0,'N',1994,106); + +DROP TABLE IF EXISTS `language`; +CREATE TABLE `language` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(15) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `mannschaft`; +CREATE TABLE `mannschaft` ( + `ID` int(11) NOT NULL auto_increment, + `team_id` int(11) NOT NULL default '0', + `sportart_id` int(11) NOT NULL default '0', + PRIMARY KEY (`ID`) +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `parallelset`; +CREATE TABLE `parallelset` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `parallelset` (`ID`,`hersteller_id`,`name`) VALUES (1,2,'Mystique Gold'),(2,1,'Pacific Copper'),(3,1,'Pacific Gold'); + +DROP TABLE IF EXISTS `position`; +CREATE TABLE `position` ( + `ID` int(11) NOT NULL auto_increment, + `sportart_id` int(11) NOT NULL default '0', + `name` varchar(20) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `position` (`ID`,`sportart_id`,`name`) VALUES (1,1,'QB'),(2,1,'WR'),(3,1,'RB'),(4,1,'LB'),(5,1,'TE'),(6,1,'FB'),(7,1,'SS'),(8,1,'DE'),(9,1,'K'),(10,1,'P'),(11,1,'LG'),(12,1,'RG'),(13,1,'OF'),(14,1,'DB'),(15,1,'CB'),(16,2,'C'),(17,2,'1B'),(18,2,'2B'),(19,2,'3B'),(20,2,'SS'),(21,2,'LF'),(22,2,'CF'),(23,2,'RF'),(24,2,'DH'),(25,2,'P'); + +DROP TABLE IF EXISTS `serie`; +CREATE TABLE `serie` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `serie` (`ID`,`hersteller_id`,`name`) VALUES (1,1,'Pacific'),(2,2,'Fleer'),(3,3,'Bowman'),(4,4,'Leaf'),(5,2,'Ultra'),(6,2,'Mystique'),(7,1,'Finest Hour'),(8,5,'SP'),(9,5,'SPX'),(10,5,'SP Authentic'),(11,5,'Black Diamond'); + +DROP TABLE IF EXISTS `spiele`; +CREATE TABLE `spiele` ( + `datum` date default NULL, + `gast` int(11) NOT NULL default '0', + `gast_pkt` int(11) default NULL, + `heim` int(11) NOT NULL default '0', + `heim_pkt` int(11) default NULL +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `spieler`; +CREATE TABLE `spieler` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `spieler` (`ID`,`name`) VALUES (1,'Pathon, Jerome'),(2,'Bruschi, Tedy'),(3,'Couch, Tim'),(4,'Shea, Aaron'),(5,'Lewis, Jamal'),(6,'Lewis, Jermaine'),(7,'Banks, Tony'),(8,'Fuamatu-Ma\'Afala, Chris'),(9,'Bettis, Jerome'),(10,'Stewart, Kordell'),(11,'Moon, Warren'),(12,'Lockett, Kevin'),(13,'Gannon, Rich'),(14,'Jett, James'),(15,'Strong, Mack'),(16,'Huard, Brock'),(17,'Watters, Ricky'),(18,'Aikman, Troy'),(19,'LaFleur, David'),(20,'Brazzell, Chris'),(21,'Dayne, Ron'),(22,'Brown, Na'),(23,'Small, Torrance'),(24,'Lewis, Chad'),(25,'Murrell, Adrian'),(26,'Smith, Maurice'),(27,'Chandler, Chris'),(28,'Kanell, Danny'),(29,'Williams, Ricky'),(30,'Garcia, Jeff'),(31,'Streets, Tai'),(32,'Garner, Charlie'),(33,'Rice, Jerry'),(34,'Owens, Terrell'),(35,'Bruce, Isaac'),(36,'Canidate, Trung'); + +DROP TABLE IF EXISTS `spielerposition`; +CREATE TABLE `spielerposition` ( + `spieler_id` int(11) NOT NULL default '0', + `sportart_id` int(11) NOT NULL default '0', + `position_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `sportart`; +CREATE TABLE `sportart` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(30) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `sportart` (`ID`,`name`) VALUES (1,'Football'),(2,'Baseball'),(3,'Basketball'),(4,'Hockey'); + +DROP TABLE IF EXISTS `suche`; +CREATE TABLE `suche` ( + `user_id` int(11) NOT NULL default '0', + `karte_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `team`; +CREATE TABLE `team` ( + `ID` int(11) NOT NULL auto_increment, + `sportart_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + `short` varchar(15) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (1,1,'Buffalo Bills','Bills'),(2,1,'Indianapolis Colts','Colts'),(3,1,'Miami Dolphins','Dolphins'),(4,1,'New England Patriots','Patriots'),(5,1,'New York Jets','Jets'),(6,1,'Baltimore Ravens','Ravens'),(7,1,'Cincinnati Bengals','Bengals'),(8,1,'Cleveland Browns','Browns'),(9,1,'Jacksonville Jaguars','Jaguars'),(10,1,'Pittsburgh Steelers','Steelers'),(11,1,'Tennessee Titans','Titans'),(12,1,'Denver Broncos','Broncos'),(13,1,'Kansas City Chiefs','Chiefs'),(14,1,'Oakland Raiders','Raiders'),(15,1,'San Diego Chargers','Chargers'),(16,1,'Seattle Seahawks','Seahawks'),(17,1,'Arizona Cardinals','Cardinals'),(18,1,'Dallas Cowboys','Cowboys'),(19,1,'New York Giants','Giants'),(20,1,'Philadelphia Eagles','Eagles'),(21,1,'Washington Redskins','Redskins'),(22,1,'Chicago Bears','Bears'),(23,1,'Detroit Lions','Lions'),(24,1,'Green Bay Packers','Packers'),(25,1,'Minnesota Vikings','Vikings'),(26,1,'Tampa Bay Buccaneers','Buccaneers'),(27,1,'Atlanta Falcons','Falcons'),(28,1,'Carolina Panthers','Panthers'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (29,1,'New Orleans Saints','Saints'),(30,1,'St.Louis Rams','Rams'),(31,1,'San Francisco 49ers','49ers'),(32,2,'Baltimore Orioles','Orioles'),(33,2,'Boston Red Sox','Red Sox'),(34,2,'New York Yankees','Yankees'),(35,2,'Tampa Bay Devil Rays','Devil Rays'),(36,2,'Toronto Blue Jays','Blue Jays'),(37,2,'Chicago White Sox','White Sox'),(38,2,'Cleveland Indians','Indians'),(39,2,'Detroit Tigers','Tigers'),(40,2,'Kansas City Royals','Royals'),(41,2,'Minnesota Twins','Twins'),(42,2,'Anaheim Angels','Angels'),(43,2,'Oakland Athletics','Athletics'),(44,2,'Seattle Mariners','Mariners'),(45,2,'Texas Rangers','Rangers'),(46,2,'Atlanta Braves','Braves'),(47,2,'Florida Marlins','Marlins'),(48,2,'Montreal Expos','Expos'),(49,2,'New York Mets','Mets'),(50,2,'Philadelphia Phillies','Phillies'),(51,2,'Chicago Cubs','Cubs'),(52,2,'Cincinnati Reds','Reds'),(53,2,'Houston Astros','Astros'),(54,2,'Milwaukee Brewers','Brewers'),(55,2,'Pittsburgh Pirates','Pirates'),(56,2,'St.Louis Cardinals','Cardinals'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (57,2,'Arizona Diamondbacks','Diamondbacks'),(58,2,'Colorado Rockies','Rockies'),(59,2,'Los Angeles Dodgers','Dodgers'),(60,2,'San Diego Padres','Padres'),(61,2,'San Francisco Giants','Giants'),(62,3,'Boston Celtics','Celtics'),(63,3,'Miami Heat','Heat'),(64,3,'New Jersey Nets','Mets'),(65,3,'New York Knicks','Knicks'),(66,3,'Orlando Magic','Magic'),(67,3,'Philadelphia 76ers','76ers'),(68,3,'Washington Wizards','Wizards'),(69,3,'Atlanta Hawks','Hawks'),(70,3,'Charlotte Hornets','Hornets'),(71,3,'Chicago Bulls','Bulls'),(72,3,'Cleveland Cavaliers','Cavaliers'),(73,3,'Detroit Pistons','Pistons'),(74,3,'Indiana Pacers','Pacers'),(75,3,'Milwaukee Bucks','Bucks'),(76,3,'Toronto Raptors','Raptors'),(77,3,'Dallas Mavericks','Mavericks'),(78,3,'Denver Nuggets','Nuggets'),(79,3,'Houston Rockets','Rockets'),(80,3,'Minnesota Timberwolves','Timberwolves'),(81,3,'San Antonio Spurs','Spurs'),(82,3,'Utah Jazz','Jazz'),(83,3,'Vancouver Grizzlies','Grizzlies'),(84,3,'Golden State Warriors','Warriors'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (85,3,'Los Angeles Clippers','Clippers'),(86,3,'Los Angeles Lakers','Lakers'),(87,3,'Phoenix Suns','Suns'),(88,3,'Portland Trail Blazers','Blazers'),(89,3,'Sacramento Kings','Kings'),(90,3,'Seattle SuperSonics','SuperSonics'),(91,4,'Boston Bruins','Bruins'),(92,4,'Buffalo Sabres','Sabres'),(93,4,'Montreal Canadiens','Canadiens'),(94,4,'Ottawa Senators','Senators'),(95,4,'Toronto Maple Leafs','Maple Leafs'),(96,4,'New Jersey Devils','Devils'),(97,4,'New York Islander','Islander'),(98,4,'New York Rangers','Rangers'),(99,4,'Philadelphia Flyers','Flyers'),(100,4,'Pittsburgh Penguins','Penguins'),(101,4,'Atlanta Trashers','Trashers'),(102,4,'Carolina Hurricanes','Hurricanes'),(103,4,'Florida Panthers','Panthers'),(104,4,'Tampa Bay Lightnings','Lightnings'),(105,4,'Washington Capitals','Capitals'),(106,4,'Chicago Blackhawks','Blackhawks'),(107,4,'Columbo Blue Jackets','Blue Jackets'),(108,4,'Detroit Red Wings','Red Wings'),(109,4,'Nashville Predators','Predators'),(110,4,'St.Louis Blues','Blues'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (111,4,'Calgary Flames','Flames'),(112,4,'Colorado Avalanche','Avalanche'),(113,4,'Edmonton Oilers','Oilers'),(114,4,'Minnesota Wild','Wild'),(115,4,'Vancouver Canucks','Canucks'),(116,4,'Anaheim Mighty Ducks','Mighty Ducks'),(117,4,'Dallas Stars','Stars'),(118,4,'Los Angeles Kings','Kings'),(119,4,'Phoenix Coyotes','Coyotes'),(120,4,'San Jose Sharks','Sharks'),(121,1,'Houston Texans','Texans'),(122,1,'Houston Oilers','Oilers'); +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; diff --git a/kotlin-quarkus/.dockerignore b/kotlin-quarkus/.dockerignore new file mode 100644 index 0000000..4361d2f --- /dev/null +++ b/kotlin-quarkus/.dockerignore @@ -0,0 +1,5 @@ +* +!build/*-runner +!build/*-runner.jar +!build/lib/* +!build/quarkus-app/* \ No newline at end of file diff --git a/kotlin-quarkus/.gitignore b/kotlin-quarkus/.gitignore new file mode 100644 index 0000000..285b6ba --- /dev/null +++ b/kotlin-quarkus/.gitignore @@ -0,0 +1,36 @@ +# Gradle +.gradle/ +build/ + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env diff --git a/kotlin-quarkus/README.md b/kotlin-quarkus/README.md new file mode 100644 index 0000000..08f2a76 --- /dev/null +++ b/kotlin-quarkus/README.md @@ -0,0 +1,78 @@ +# kontor Project + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: +```shell script +./gradlew quarkusDev +``` + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. + +## Packaging and running the application + +The application can be packaged using: +```shell script +./gradlew build +``` +It produces the `quarkus-run.jar` file in the `build/quarkus-app/` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `build/quarkus-app/lib/` directory. + +The application is now runnable using `java -jar build/quarkus-app/quarkus-run.jar`. + +If you want to build an _über-jar_, execute the following command: +```shell script +./gradlew build -Dquarkus.package.type=uber-jar +``` + +The application, packaged as an _über-jar_, is now runnable using `java -jar build/*-runner.jar`. + +## Creating a native executable + +You can create a native executable using: +```shell script +./gradlew build -Dquarkus.package.type=native +``` + +Or, if you don't have GraalVM installed, you can run the native executable build in a container using: +```shell script +./gradlew build -Dquarkus.package.type=native -Dquarkus.native.container-build=true +``` + +You can then execute your native executable with: `./build/kontor-1.0.0-SNAPSHOT-runner` + +If you want to learn more about building native executables, please consult https://quarkus.io/guides/gradle-tooling. + +## Related Guides + +- JDBC Driver - H2 ([guide](https://quarkus.io/guides/datasource)): Connect to the H2 database via JDBC +- Hibernate ORM with Panache and Kotlin ([guide](https://quarkus.io/guides/hibernate-orm-panache-kotlin)): Define your persistent model in Hibernate ORM with Panache +- SmallRye OpenAPI ([guide](https://quarkus.io/guides/openapi-swaggerui)): Document your REST APIs with OpenAPI - comes with Swagger UI +- YAML Configuration ([guide](https://quarkus.io/guides/config#yaml)): Use YAML to configure your Quarkus application +- SmallRye Health ([guide](https://quarkus.io/guides/microprofile-health)): Monitor service health + +## Provided Code + +### YAML Config + +Configure your application with YAML + +[Related guide section...](https://quarkus.io/guides/config-reference#configuration-examples) + +The Quarkus application configuration is located in `src/main/resources/application.yml`. + +### RESTEasy Reactive + +Easily start your Reactive RESTful Web Services + +[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources) + +### SmallRye Health + +Monitor your application's health using SmallRye Health + +[Related guide section...](https://quarkus.io/guides/smallrye-health) diff --git a/kotlin-quarkus/build.gradle.kts b/kotlin-quarkus/build.gradle.kts new file mode 100644 index 0000000..0a6ac3a --- /dev/null +++ b/kotlin-quarkus/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + java + id("io.quarkus") +} + +repositories { + mavenCentral() + mavenLocal() +} + +val quarkusPlatformGroupId: String by project +val quarkusPlatformArtifactId: String by project +val quarkusPlatformVersion: String by project + +dependencies { + implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) + implementation("io.quarkus:quarkus-resteasy-reactive-jackson") + implementation("io.quarkus:quarkus-jdbc-h2") + implementation("io.quarkus:quarkus-hibernate-orm-panache-kotlin") + implementation("io.quarkus:quarkus-smallrye-openapi") + implementation("io.quarkus:quarkus-config-yaml") + implementation("io.quarkus:quarkus-hibernate-reactive-panache-kotlin") + implementation("io.quarkus:quarkus-smallrye-health") + implementation("io.quarkus:quarkus-arc") + implementation("io.quarkus:quarkus-resteasy-reactive") + testImplementation("io.quarkus:quarkus-junit5") + testImplementation("io.rest-assured:rest-assured") +} + +group = "de.thpeetz.kontor" +version = "1.0.0-SNAPSHOT" + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.withType { + systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager") +} + +tasks.withType { + options.encoding = "UTF-8" + options.compilerArgs.add("-parameters") +} diff --git a/kotlin-quarkus/gradle.properties b/kotlin-quarkus/gradle.properties new file mode 100644 index 0000000..1057e5c --- /dev/null +++ b/kotlin-quarkus/gradle.properties @@ -0,0 +1,6 @@ +#Gradle properties +quarkusPluginId=io.quarkus +quarkusPluginVersion=2.15.2.Final +quarkusPlatformGroupId=io.quarkus.platform +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformVersion=2.15.2.Final \ No newline at end of file diff --git a/kotlin-quarkus/gradle/wrapper/gradle-wrapper.jar b/kotlin-quarkus/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..62d4c05 Binary files /dev/null and b/kotlin-quarkus/gradle/wrapper/gradle-wrapper.jar differ diff --git a/kotlin-quarkus/gradle/wrapper/gradle-wrapper.properties b/kotlin-quarkus/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/kotlin-quarkus/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kotlin-quarkus/gradlew b/kotlin-quarkus/gradlew new file mode 100755 index 0000000..fbd7c51 --- /dev/null +++ b/kotlin-quarkus/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/kotlin-quarkus/gradlew.bat b/kotlin-quarkus/gradlew.bat new file mode 100644 index 0000000..a9f778a --- /dev/null +++ b/kotlin-quarkus/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kotlin-quarkus/settings.gradle.kts b/kotlin-quarkus/settings.gradle.kts new file mode 100644 index 0000000..043f9fc --- /dev/null +++ b/kotlin-quarkus/settings.gradle.kts @@ -0,0 +1,13 @@ +pluginManagement { + val quarkusPluginVersion: String by settings + val quarkusPluginId: String by settings + repositories { + mavenCentral() + gradlePluginPortal() + mavenLocal() + } + plugins { + id(quarkusPluginId) version quarkusPluginVersion + } +} +rootProject.name="kontor" diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.jvm b/kotlin-quarkus/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..669d901 --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.jvm @@ -0,0 +1,94 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/kontor-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 build/quarkus-app/*.jar /deployments/ +COPY --chown=185 build/quarkus-app/app/ /deployments/app/ +COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.legacy-jar b/kotlin-quarkus/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 0000000..ab8ff1f --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,90 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/kontor-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +COPY build/lib/* /deployments/lib/ +COPY build/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.native b/kotlin-quarkus/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..38e4141 --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/kontor . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.native-micro b/kotlin-quarkus/src/main/docker/Dockerfile.native-micro new file mode 100644 index 0000000..cce62e8 --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/kontor . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor +# +### +FROM quay.io/quarkus/quarkus-micro-image:1.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/kotlin-quarkus/src/main/resources/META-INF/resources/index.html b/kotlin-quarkus/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000..f64aa10 --- /dev/null +++ b/kotlin-quarkus/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,289 @@ + + + + + kontor - 1.0.0-SNAPSHOT + + + +
+
+
+ + + + + quarkus_logo_horizontal_rgb_1280px_reverse + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+

You just made a Quarkus application.

+

This page is served by Quarkus.

+ Visit the Dev UI +

This page: src/main/resources/META-INF/resources/index.html

+

App configuration: src/main/resources/application.yml

+

Static assets: src/main/resources/META-INF/resources/

+

Code: src/main/java

+

Generated starter code:

+
    +
  • + RESTEasy Reactive Easily start your Reactive RESTful Web Services +
    @Path: /hello +
    Related guide +
  • + +
+
+
+

Selected extensions

+
    +
  • RESTEasy Reactive Jackson
  • +
  • JDBC Driver - H2 (guide)
  • +
  • Hibernate ORM with Panache and Kotlin (guide)
  • +
  • SmallRye OpenAPI (guide)
  • +
  • YAML Configuration (guide)
  • +
  • Hibernate Reactive Panache Kotlin
  • +
  • SmallRye Health (guide)
  • +
+
Documentation
+

Practical step-by-step guides to help you achieve a specific goal. Use them to help get your work + done.

+
Set up your IDE
+

Everyone has a favorite IDE they like to use to code. Learn how to configure yours to maximize your + Quarkus productivity.

+
+
+
+ + diff --git a/kotlin-quarkus/src/main/resources/application.yml b/kotlin-quarkus/src/main/resources/application.yml new file mode 100644 index 0000000..527a35f --- /dev/null +++ b/kotlin-quarkus/src/main/resources/application.yml @@ -0,0 +1,2 @@ +greeting: + message: "hello" diff --git a/kotlin-quarkus/src/native-test/java/de/thpeetz/kontor/GreetingResourceIT.java b/kotlin-quarkus/src/native-test/java/de/thpeetz/kontor/GreetingResourceIT.java new file mode 100644 index 0000000..ef71448 --- /dev/null +++ b/kotlin-quarkus/src/native-test/java/de/thpeetz/kontor/GreetingResourceIT.java @@ -0,0 +1,8 @@ +package de.thpeetz.kontor; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class GreetingResourceIT extends GreetingResourceTest { + // Execute the same tests but in packaged mode. +} diff --git a/kotlin-spring/.gitignore b/kotlin-spring/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/kotlin-spring/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/kotlin-spring/README.md b/kotlin-spring/README.md new file mode 100644 index 0000000..0473319 --- /dev/null +++ b/kotlin-spring/README.md @@ -0,0 +1,92 @@ +# Kontor Kotlin + + + +## Getting started + +To make it easy for you to get started with GitLab, here's a list of recommended next steps. + +Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! + +## Add your files + +- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files +- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: + +``` +cd existing_repo +git remote add origin https://gitlab.thpeetz.de/kontor/kontor-kotlin.git +git branch -M main +git push -uf origin main +``` + +## Integrate with your tools + +- [ ] [Set up project integrations](https://gitlab.thpeetz.de/kontor/kontor-kotlin/-/settings/integrations) + +## Collaborate with your team + +- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) +- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) +- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) +- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) +- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) + +## Test and Deploy + +Use the built-in continuous integration in GitLab. + +- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) +- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) +- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) +- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) +- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) + +*** + +# Editing this README + +When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. + +## Suggestions for a good README +Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. + +## Name +Choose a self-explaining name for your project. + +## Description +Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. + +## Badges +On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. + +## Visuals +Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. + +## Installation +Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. + +## Usage +Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. + +## Support +Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. + +## Roadmap +If you have ideas for releases in the future, it is a good idea to list them in the README. + +## Contributing +State if you are open to contributions and what your requirements are for accepting them. + +For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. + +You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. + +## Authors and acknowledgment +Show your appreciation to those who have contributed to the project. + +## License +For open source projects, say how it is licensed. + +## Project status +If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/kotlin-spring/build.gradle b/kotlin-spring/build.gradle new file mode 100644 index 0000000..c6df720 --- /dev/null +++ b/kotlin-spring/build.gradle @@ -0,0 +1,58 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id 'org.springframework.boot' version '2.7.7' + id 'io.spring.dependency-management' version '1.0.15.RELEASE' + id 'org.jetbrains.kotlin.jvm' version '1.7.21' + id 'org.jetbrains.kotlin.plugin.spring' version '1.7.21' + id 'org.jetbrains.kotlin.plugin.jpa' version '1.7.21' + id 'org.jetbrains.kotlin.plugin.allopen' version '1.7.21' + id 'org.jetbrains.kotlin.kapt' version '1.7.21' +} + +group = 'de.thpeetz.kontor' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = '11' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-mustache' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' + implementation 'org.jetbrains.kotlin:kotlin-reflect' + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + kapt 'org.springframework.boot:spring-boot-configuration-processor' + runtimeOnly 'com.h2database:h2' + runtimeOnly 'org.springframework.boot:spring-boot-devtools' + testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + exclude module: 'mockito-core' + } + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'com.ninja-squad:springmockk:3.1.1' + + +} + +tasks.withType(KotlinCompile) { + kotlinOptions { + freeCompilerArgs = ['-Xjsr305=strict'] + jvmTarget = '11' + } +} + +tasks.named('test') { + useJUnitPlatform() +} + +allOpen { + annotation("javax.persistence.Entity") + annotation("javax.persistence.Embeddable") + annotation("javax.persistence.MappedSuperclass") +} diff --git a/kotlin-spring/gradle/wrapper/gradle-wrapper.jar b/kotlin-spring/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/kotlin-spring/gradle/wrapper/gradle-wrapper.jar differ diff --git a/kotlin-spring/gradle/wrapper/gradle-wrapper.properties b/kotlin-spring/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..070cb70 --- /dev/null +++ b/kotlin-spring/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kotlin-spring/gradlew b/kotlin-spring/gradlew new file mode 100644 index 0000000..a69d9cb --- /dev/null +++ b/kotlin-spring/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/kotlin-spring/gradlew.bat b/kotlin-spring/gradlew.bat new file mode 100644 index 0000000..53a6b23 --- /dev/null +++ b/kotlin-spring/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kotlin-spring/settings.gradle b/kotlin-spring/settings.gradle new file mode 100644 index 0000000..185aa4e --- /dev/null +++ b/kotlin-spring/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'kontor' diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorApplication.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorApplication.kt new file mode 100644 index 0000000..279b7db --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorApplication.kt @@ -0,0 +1,16 @@ +package de.thpeetz.kontor + +import org.springframework.boot.Banner +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.runApplication + +@SpringBootApplication +@EnableConfigurationProperties(KontorProperties::class) +class KontorApplication + +fun main(args: Array) { + runApplication(*args) { + setBannerMode(Banner.Mode.OFF) + } +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorConfiguration.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorConfiguration.kt new file mode 100644 index 0000000..e54f2f9 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorConfiguration.kt @@ -0,0 +1,79 @@ +package de.thpeetz.kontor + +import de.thpeetz.kontor.comics.* +import org.springframework.boot.ApplicationRunner +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class KontorConfiguration { + @Bean + fun databaseInitializer(artistRepository: ArtistRepository, + publisherRepository: PublisherRepository, + comicRepository: ComicRepository, + issueRepository: IssueRepository + ) = ApplicationRunner { + + artistRepository.save(Artist("Turner, Michael")) + artistRepository.save(Artist("Bendis, Brian Michael")) + artistRepository.save(Artist("Land, Greg")) + artistRepository.save(Artist("Whedon, Joss")) + val marvel = publisherRepository.save(Publisher("Marvel")) + val aspen = publisherRepository.save(Publisher("Aspen")) + publisherRepository.save(Publisher("DC")) + val de = publisherRepository.save(Publisher("Dynamite Entertainment")) + val wildstorm = publisherRepository.save(Publisher("WildStorm")) + val bongo = publisherRepository.save(Publisher("Bongo")) + val image = publisherRepository.save(Publisher("Image")) + val darkHorse = publisherRepository.save(Publisher("Dark Horse Comics")) + val cliffhanger = publisherRepository.save(Publisher("Cliffhanger")) + comicRepository.save(Comic(title = "X-Men", publisher = marvel, currentOrder = false, completed = false)) + val redSonja = comicRepository.save(Comic(title = "Red Sonja", publisher = de, currentOrder = false, completed = false)) + val x23 = comicRepository.save(Comic(title = "X-23", publisher = marvel, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Simpsons Comics", publisher = bongo, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Futurama Comics", publisher = bongo, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Bomb Queen III: The Good, The Bad and The Lovely", publisher = image, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Bomb Queen IV: Suicide Bomber", publisher = image, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Gen13", publisher = wildstorm, currentOrder = false, completed = false)) + val bombqueen = comicRepository.save(Comic(title = "Bomb Queen II: Queen of Hearts", publisher = image, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Iron & The Maiden",publisher = aspen,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Fathom",publisher = aspen,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Soulfire",publisher = aspen,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Rebellion",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Rebellion",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Knights of the Old Republic",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Legacy",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Dark Times", publisher = darkHorse, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Samurai: Heaven and Earth", publisher = darkHorse, currentOrder = false, completed = false)) + val battlpope = comicRepository.save(Comic(title = "Battle Pope", publisher = image,currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Danger Girl", publisher = cliffhanger, currentOrder = false, completed = false)) + val marville = comicRepository.save(Comic(title = "Marville", publisher = marvel, currentOrder = false, completed = false)) + issueRepository.save(Issue(number = "0", comic = redSonja, gelesen = true)) + issueRepository.save(Issue(number = "1", comic = redSonja, gelesen = true)) + issueRepository.save(Issue(number = "2", comic = redSonja, gelesen = true)) + issueRepository.save(Issue(number = "1", comic = x23, gelesen = false)) + issueRepository.save(Issue(number = "1", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "2", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "3", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "4", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "1", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "2", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "3", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "4", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "5", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "6", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "7", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "8", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "9", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "10", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "11", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "12", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "1", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "2", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "3", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "4", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "5", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "6", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "7", comic = marville, gelesen = false)) + } +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorController.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorController.kt new file mode 100644 index 0000000..3d639dd --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorController.kt @@ -0,0 +1,24 @@ +package de.thpeetz.kontor + +import de.thpeetz.kontor.KontorProperties +import de.thpeetz.kontor.Navigation +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.ui.set +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/") +class KontorController(val properties: KontorProperties, val navigation: Navigation) { + @GetMapping("/") + fun blog(model: Model): String { + model["title"] = properties.title + model["title"] = "Kontor" + model["banner"] = properties.banner + model["version"] = properties.version + model["navigation"] = navigation.links() + model["login"] = navigation.login() + return "kontor" + } +} \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorProperties.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorProperties.kt new file mode 100644 index 0000000..92c77c7 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorProperties.kt @@ -0,0 +1,10 @@ +package de.thpeetz.kontor + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.ConstructorBinding + +@ConstructorBinding +@ConfigurationProperties("kontor") +data class KontorProperties(var title: String, var version: String = "unknown", val banner: Banner) { + data class Banner(val title: String? = null, val content: String) +} \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/Navigation.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/Navigation.kt new file mode 100644 index 0000000..fb730af --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/Navigation.kt @@ -0,0 +1,24 @@ +package de.thpeetz.kontor + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class Navigation { + + @Bean + fun links(): Iterable = listOf( + Link("/comics", "Comics"), + Link("/library","Library"), + Link("/office", "HomeOffice"), + Link("/tradingcards", "Trading Cards"), + Link("/tysc","TradeYourSportsCards"), + Link("/user/login", ""), + Link("/admin/","Admin") + ) + + @Bean + fun login(): Iterable = listOf(Link("/user/login", "Login")) +} + +data class Link(val link: String, val title: String) diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/RequestLoggingFilter.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/RequestLoggingFilter.kt new file mode 100644 index 0000000..decb70f --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/RequestLoggingFilter.kt @@ -0,0 +1,25 @@ +package de.thpeetz.kontor + +import org.slf4j.LoggerFactory +import javax.servlet.Filter +import org.springframework.stereotype.Component +import javax.servlet.FilterChain +import javax.servlet.ServletRequest +import javax.servlet.ServletResponse + +@Component +class RequestLoggingFilter: Filter { + val loggerFactory = LoggerFactory.getLogger("Kontor Logger") + + override fun doFilter( + request: ServletRequest, + response: ServletResponse, + filterChain: FilterChain + ) { + val requestString = request.servletContext.contextPath.toString() + //val attributeNames = request.attributeNames + //attributeNames.toList().forEach { loggerFactory.info("Attribute: ${it.toString()}") } + loggerFactory.info("Logging request: $requestString") + filterChain.doFilter(request, response) + } +} \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsApiController.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsApiController.kt new file mode 100644 index 0000000..d2e3b4e --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsApiController.kt @@ -0,0 +1,25 @@ +package de.thpeetz.kontor.comics + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("api/") +class ComicsApiController(val artistRepository: ArtistRepository, + val publisherRepository: PublisherRepository, + val comicRepository: ComicRepository, + val issueRepository: IssueRepository) { + + @GetMapping("/artist") + fun artistList() = artistRepository.findAll() + + @GetMapping("/publisher") + fun publisherList() = publisherRepository.findAll() + + @GetMapping("/comics") + fun comicsList() = comicRepository.findAll() + + @GetMapping("/issues") + fun issuesList() = issueRepository.findAll() +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsController.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsController.kt new file mode 100644 index 0000000..f339b04 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsController.kt @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.comics + +import de.thpeetz.kontor.KontorProperties +import de.thpeetz.kontor.Navigation +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.ui.set +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/comics") +class ComicsController(private val properties: KontorProperties, + val navigation: Navigation, + val comicRepository: ComicRepository +) { + + @GetMapping("/") + fun blog(model: Model): String { + model["title"] = properties.title + model["title"] = "Comics" + model["banner"] = properties.banner + model["navigation"] = navigation.links() + model["comics"] = comicRepository.findAll().map { it } + return "comics" + } + + +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Entities.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Entities.kt new file mode 100644 index 0000000..3be00f5 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Entities.kt @@ -0,0 +1,36 @@ +package de.thpeetz.kontor.comics + +import javax.persistence.Entity +import javax.persistence.GeneratedValue +import javax.persistence.Id +import javax.persistence.ManyToOne + + +@Entity +class Artist( + val name: String, + @Id @GeneratedValue val id: Long? = null +) + +@Entity +class Publisher( + val name: String, + @Id @GeneratedValue val id: Long? = null +) + +@Entity +class Comic( + val title: String, + @ManyToOne val publisher: Publisher, + val currentOrder: Boolean, + val completed: Boolean, + @Id @GeneratedValue val id: Long? = null +) + +@Entity +class Issue( + val number: String, + @ManyToOne val comic: Comic, + val gelesen: Boolean, + @Id @GeneratedValue val id: Long? = null +) \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Repositories.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Repositories.kt new file mode 100644 index 0000000..fe44b2e --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Repositories.kt @@ -0,0 +1,22 @@ +package de.thpeetz.kontor.comics + +import org.springframework.data.repository.CrudRepository + +interface ArtistRepository: CrudRepository { + + override fun findAll(): Iterable +} + +interface PublisherRepository: CrudRepository { + override fun findAll(): Iterable +} + +interface ComicRepository: CrudRepository { + + override fun findAll(): Iterable +} + +interface IssueRepository: CrudRepository { + + override fun findAll(): Iterable +} \ No newline at end of file diff --git a/kotlin-spring/src/main/resources/application.properties b/kotlin-spring/src/main/resources/application.properties new file mode 100644 index 0000000..6d27d3a --- /dev/null +++ b/kotlin-spring/src/main/resources/application.properties @@ -0,0 +1,6 @@ +spring.jpa.properties.hibernate.globally_quoted_identifiers=true +spring.jpa.properties.hibernate.globally_quoted_identifiers_skip_column_definitions = true +kontor.title=Kontor +kontor.version=1.0.0-SNAPSHOT +kontor.banner.title=Warning +kontor.banner.content=The blog will be down tomorrow. diff --git a/kotlin-spring/src/main/resources/templates/comics.mustache b/kotlin-spring/src/main/resources/templates/comics.mustache new file mode 100644 index 0000000..336d9f0 --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/comics.mustache @@ -0,0 +1,23 @@ +{{> header}} + +{{> menu}} + + +
+ + + + {{#comics}} + + {{/comics}} +
List of Comics
Name
{{title}}
+
+ Add entry +
+
+ +{{> footer}} diff --git a/kotlin-spring/src/main/resources/templates/footer.mustache b/kotlin-spring/src/main/resources/templates/footer.mustache new file mode 100644 index 0000000..dde9847 --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/footer.mustache @@ -0,0 +1,22 @@ + + + + diff --git a/kotlin-spring/src/main/resources/templates/header.mustache b/kotlin-spring/src/main/resources/templates/header.mustache new file mode 100644 index 0000000..d794c9e --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/header.mustache @@ -0,0 +1,15 @@ + + + + + {{title}} + + + + + + + + + + diff --git a/kotlin-spring/src/main/resources/templates/kontor.mustache b/kotlin-spring/src/main/resources/templates/kontor.mustache new file mode 100644 index 0000000..177306f --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/kontor.mustache @@ -0,0 +1,18 @@ +{{> header}} + +{{> menu}} + +
+ {{#banner.title}} +
+ + +
+ {{/banner.title}} +
+ +{{> footer}} diff --git a/kotlin-spring/src/main/resources/templates/menu.mustache b/kotlin-spring/src/main/resources/templates/menu.mustache new file mode 100644 index 0000000..c4d9ba0 --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/menu.mustache @@ -0,0 +1,23 @@ + diff --git a/kotlin-spring/src/test/resources/junit-platform.properties b/kotlin-spring/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..ab79697 --- /dev/null +++ b/kotlin-spring/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.testinstance.lifecycle.default = per_class diff --git a/wicket/.github/workflows/gradle.yml b/wicket/.github/workflows/gradle.yml new file mode 100644 index 0000000..58e1c59 --- /dev/null +++ b/wicket/.github/workflows/gradle.yml @@ -0,0 +1,26 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build diff --git a/wicket/.gitignore b/wicket/.gitignore new file mode 100644 index 0000000..2e1f2c3 --- /dev/null +++ b/wicket/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +bin/ +.classpath +.project +.settings/ +.asciidoctorconfig.adoc diff --git a/wicket/.gitlab-ci.yml b/wicket/.gitlab-ci.yml new file mode 100644 index 0000000..514cefd --- /dev/null +++ b/wicket/.gitlab-ci.yml @@ -0,0 +1,36 @@ +variables: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + +before_script: + - source "/home/gitlab-runner/.sdkman/bin/sdkman-init.sh" + - sdk u java 11.0.12-open + +stages: + - build + - test + - analysis + - publish + +Build Application: + stage: build + script: ./gradlew build + +Create Documentation: + stage: build + script: ./gradlew asciidoctorPdf + +Test Application: + stage: test + script: ./gradlew check + +sonarqube-check: + stage: analysis + variables: + SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache + GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task + script: ./gradlew sonarqube + allow_failure: true + +Publish Artifacts: + stage: publish + script: ./gradlew publish diff --git a/wicket/README.md b/wicket/README.md new file mode 100644 index 0000000..ab12d07 --- /dev/null +++ b/wicket/README.md @@ -0,0 +1,4 @@ +![Java CI with Gradle](https://github.com/tpeetz/kontor-wicket/workflows/Java%20CI%20with%20Gradle/badge.svg) + +# kontor-wicket +Kontor Application with Apache Wicket diff --git a/wicket/build.gradle b/wicket/build.gradle new file mode 100644 index 0000000..0afbe9f --- /dev/null +++ b/wicket/build.gradle @@ -0,0 +1,61 @@ +plugins { + id 'war' + id 'jacoco-report-aggregation' + alias(versions.plugins.asciidoctorConvention) + alias(versions.plugins.javaConvention) + alias(versions.plugins.sonarqube) +} + +final BUILD_DATE = new Date().format('dd.MM.yyyy').toString() + +dependencies { + implementation versions.slf4j + implementation versions.commonscli + testImplementation versions.junit + implementation versions.bundles.logback + spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.12.0' + + implementation 'org.apache.wicket:wicket:9.9.1' + implementation 'org.apache.wicket:wicket-extensions:9.9.1' + testImplementation 'org.eclipse.jetty:jetty-webapp:9.4.44.v20210927' + testImplementation 'org.eclipse.jetty:jetty-jmx:9.4.44.v20210927' +} + +publishing { + publications { + application(MavenPublication) { + groupId = group + from components.java + } + } +} + +jacocoTestReport { + reports { + xml.enabled true + } +} + +test.finalizedBy jacocoTestReport + +spotbugs { + ignoreFailures = true +} + +sonarqube { + properties { + property "sonar.projectKey", "kontor_kontor-wicket_AYCAQ47WRCd9N673sSSD" + property "sonar.host.url", "https://sonar.thpeetz.de" + property "sonar.login", "7cf604ab1ebf48f7dc60d942c8196a132b228a6d" + property "sonar.qualitygate.wait", true + property "sonar.sourceEncoding", "UTF-8" + } +} + +tasks.named('sonarqube').configure { + dependsOn test +} + +wrapper { + gradleVersion = "7.5" +} diff --git a/wicket/gradle.properties b/wicket/gradle.properties new file mode 100644 index 0000000..e140eca --- /dev/null +++ b/wicket/gradle.properties @@ -0,0 +1,3 @@ +description='Anwendung Kontor mit Apache Wicket' +group=de.thpeetz +version=1.0.0-SNAPSHOT diff --git a/wicket/gradle/wrapper/gradle-wrapper.jar b/wicket/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/wicket/gradle/wrapper/gradle-wrapper.jar differ diff --git a/wicket/gradle/wrapper/gradle-wrapper.properties b/wicket/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8049c68 --- /dev/null +++ b/wicket/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/wicket/gradlew b/wicket/gradlew new file mode 100755 index 0000000..a69d9cb --- /dev/null +++ b/wicket/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/wicket/gradlew.bat b/wicket/gradlew.bat new file mode 100644 index 0000000..53a6b23 --- /dev/null +++ b/wicket/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/wicket/settings.gradle b/wicket/settings.gradle new file mode 100644 index 0000000..f66ecb4 --- /dev/null +++ b/wicket/settings.gradle @@ -0,0 +1 @@ +rootProject.name = "kontor-wicket" diff --git a/wicket/src/docs/asciidoc/kontor-wicket.adoc b/wicket/src/docs/asciidoc/kontor-wicket.adoc new file mode 100644 index 0000000..e310b6f --- /dev/null +++ b/wicket/src/docs/asciidoc/kontor-wicket.adoc @@ -0,0 +1,179 @@ += Projektbeschreibung kontor-wicket: Entwicklungs- und Projekthandbuch +:author: Thomas Peetz +:email: +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: +:toclevels: 4 +:table-caption!: +:counter: table-number: 0 + +[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header"] +|=== +| Version | Datum | Autor | Änderungsgrund / Bemerkungen +| 1.0.0 | 16.05.2022 | Thomas Peetz | Ersterstellung +|=== + +== Allgemeines + +=== Zweck des Dokumentes + +Das Entwicklungshandbuch beschreibt die Werkzeuge und die Vorgehensweise bei der Entwicklung +im Projekt kontor-wicket und der Erstellung der Dokumentation. + +=== Verwendete Tools + +==== Gitlab + +Für die Verwaltung des Sourcecode kommt ((Gitlab))<> zum Einsatz. +Mit Gitlab werden auch die Projektaufgaben verwaltet. + +Das Projekt und das dazugehörige Git Repository sind unter der Adresse + +https://gitlab.ingenieurbuero-peetz.de/kontor/kontor-wicket + +zu finden. + +== Erstellung der Dokumentation + +Die Dokumentation des Projektes wird mit ((Asciidoctor))<> geschrieben. +Die Dokumente erhalten ihre Namen nach dem jeweiligen Hauptdokument. + +=== Quellcode Verwaltung + +Die Asciidoctor-Dateien haben die Endung `.adoc`. + +=== Buildsystem + +Zur Erstellung der PDF-Dateien aus den Asciidoctor-Dateien wird das Buildsystem ((Gradle))<<3>> verwendet. +Die Dateien für die Dokumente liegen im Verzeichnis `src/docs/asciidoc`. + +Der Gradle Build wird über die Datei `build.gradle` definiert. + + +== Einführung + +=== Zweck + +=== Stakeholder des Systems + +=== Systemumfang + +==== Zielsetzung des Systems + +=== Systemübersicht + +==== Systemkontext + +==== Systemarchitektur + +==== Systemschnittstellen + +===== Realisierte Schnittstellen + +===== Verwendete Schnittstellen + +==== Logisches Datenmodell + +==== Einschränkungen + +== Anforderungen der Domäne + +=== Systemfunktionen + +==== Anwendungsfälle + +==== Akteure + +==== Zielgruppen + +=== Anforderungen + +==== Anforderungen an externe Schnittstellen + +==== Funktionale Anforderungen + +==== Qualitätsanforderungen + +==== Randbedingungen + +==== Weitere Anforderungen + +==== Wartungs- und Supportinformationen + +=== Verifikation + +== Projektbeschreibung + +=== Ausgangslage + +//==== Rechtliche Vorgaben und Rahmenbedingungen +//=== Rahmenbedingungen + +//==== Vorhandene Regelungen + +=== Projektziele + +=== Projektabgrenzung + +//=== Voraussichtliche Kosten + +//=== Projektrisiken + +//==== Produktivität + +//==== Finanzielle Risiken + +//==== Akzeptanz + +== Projektorganisation + +=== Projekt-Aufbauorganisation + +=== Rollendefinition + +//==== Projektauftraggeber + +//==== Projektausschuss + +//==== Beratung / Qualitätssicherung + +==== Projekteiter + +==== Projektteam + +==== Liste der Stakeholder + +=== Projektablauforganisation + +==== Projekt-Phasen + +===== Erstellung der Projektdokumentation + + +== Verschiedenes + +=== Erreichbarkeiten + +[bibliography] +== Referenzen + +- [[[asciidoctor]]] http://asciidoctor.org +- [[[gitlab]]] http://www.gitlab.org +- [[[gradle]]] http://www.gradle.org +- [[[jenkins]]] http://jenkins-ci.org + +[glossary] +== Glossar + +[index] +== Index + +== Verzeichnisse + +=== Abbildungsverzeichnis + +=== Tabellenverzeichnis + +<> <> diff --git a/wicket/src/main/java/de/thpeetz/kontor/HomePage.java b/wicket/src/main/java/de/thpeetz/kontor/HomePage.java new file mode 100644 index 0000000..e5a3c82 --- /dev/null +++ b/wicket/src/main/java/de/thpeetz/kontor/HomePage.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor; + +import org.apache.wicket.request.mapper.parameter.PageParameters; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.WebPage; + +public class HomePage extends WebPage { + private static final long serialVersionUID = 1L; + + public HomePage(final PageParameters parameters) { + super(parameters); + + add(new Label("version", getApplication().getFrameworkSettings().getVersion())); + + // TODO Add your page's components here + + } +} diff --git a/wicket/src/main/java/de/thpeetz/kontor/KontorApplication.java b/wicket/src/main/java/de/thpeetz/kontor/KontorApplication.java new file mode 100644 index 0000000..9c39858 --- /dev/null +++ b/wicket/src/main/java/de/thpeetz/kontor/KontorApplication.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor; + +import org.apache.wicket.csp.CSPDirective; +import org.apache.wicket.csp.CSPDirectiveSrcValue; +import org.apache.wicket.markup.html.WebPage; +import org.apache.wicket.protocol.http.WebApplication; + +/** + * Application object for your web application. + * If you want to run this application without deploying, run the Start class. + * + * @see de.thpeetz.kontor.Start#main(String[]) + */ +public class KontorApplication extends WebApplication +{ + /** + * @see org.apache.wicket.Application#getHomePage() + */ + @Override + public Class getHomePage() + { + return HomePage.class; + } + + /** + * @see org.apache.wicket.Application#init() + */ + @Override + public void init() + { + super.init(); + + // needed for the styling used by the quickstart + getCspSettings().blocking() + .add(CSPDirective.STYLE_SRC, CSPDirectiveSrcValue.SELF) + .add(CSPDirective.STYLE_SRC, "https://fonts.googleapis.com/css") + .add(CSPDirective.FONT_SRC, "https://fonts.gstatic.com"); + + // add your configuration here + } +} diff --git a/wicket/src/main/resources/de/thpeetz/kontor/HomePage.html b/wicket/src/main/resources/de/thpeetz/kontor/HomePage.html new file mode 100644 index 0000000..4ca32fc --- /dev/null +++ b/wicket/src/main/resources/de/thpeetz/kontor/HomePage.html @@ -0,0 +1,62 @@ + + + + + Apache Wicket Quickstart + + + + +
+ +
+
+

Congratulations!

+

+ Your quick start works! This project is especially useful to + start developing your Wicket application or to create a test + case for a bug report. +

+

Get started

+

+ You can even switch to HTTPS! +

+

+ From here you can start hacking away at your application and + wow your clients: +

+ +

Get help

+

+ We are here to help! +

+ +

Reporting a bug

+

+ Help us help you: +

+
    +
  1. reproduce the bug with the least amount of code
  2. +
  3. create a unit test that shows the bug
  4. +
  5. fix the bug and create a patch
  6. +
  7. attach the result of step 1, 2 or 3 to a JIRA issue
  8. +
  9. profit!
  10. +
+

+ Please mention the correct Wicket version: 1.5-SNAPSHOT. +

+
+
+
+ + diff --git a/wicket/src/main/webapp/WEB-INF/web.xml b/wicket/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..f5b288e --- /dev/null +++ b/wicket/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,32 @@ + + + + kontor-wicket + + + + + wicket.kontor-wicket + org.apache.wicket.protocol.http.WicketFilter + + applicationClassName + de.thpeetz.kontor.KontorApplication + + + + + wicket.kontor-wicket + /* + + diff --git a/wicket/src/main/webapp/logo.png b/wicket/src/main/webapp/logo.png new file mode 100644 index 0000000..39ec548 Binary files /dev/null and b/wicket/src/main/webapp/logo.png differ diff --git a/wicket/src/main/webapp/style.css b/wicket/src/main/webapp/style.css new file mode 100644 index 0000000..87576a7 --- /dev/null +++ b/wicket/src/main/webapp/style.css @@ -0,0 +1,68 @@ +body, p, li, a { font-family: georgia, times, serif;font-size:13pt;} +h1, h2, h3 { font-family: 'Yanone Kaffeesatz', arial, serif; } +body { margin:0;padding:0;} +#hd { + width : 100%; + height : 87px; + background-color : #092E67; + margin-top : 0; + padding-top : 10px; + border-bottom : 1px solid #888; + z-index : 0; +} +#ft { + position : absolute; + bottom : 0; + width : 100%; + height : 99px; + background-color : #6493D2; + border-top : 1px solid #888; + z-index : 0; +} +#logo,#bd { + width : 650px; + margin: 0 auto; + padding: 25px 50px 0 50px; +} +#logo h1 { + color : white; + font-size:36pt; + display: inline; +} +#logo img { + display:inline; + vertical-align: bottom; + margin-left : 50px; + margin-right : 5px; +} +body { margin-top : 0; padding-top : 0;} +#logo, #logo h1 { margin-top : 0; padding-top : 0;} +#bd { + position : absolute; + top : 75px; + bottom : 75px; + left : 50%; + margin-left : -325px; + z-index : 1; + overflow: auto; + background-color : #fff; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + -moz-box-shadow: 0px 0px 10px #888; + -webkit-box-shadow: 0px 0px 10px #888; + box-shadow: 0px 0px 10px #888; +} +a, a:visited, a:hover, a:active { + color : #6493D2; +} +h2 { + padding : 0; margin:0; + font-size:36pt; + color:#FF5500; +} +h3 { + padding : 0; margin:0; + font-size:24pt; + color:#092E67; +} diff --git a/wicket/src/test/java/de/thpeetz/kontor/Start.java b/wicket/src/test/java/de/thpeetz/kontor/Start.java new file mode 100644 index 0000000..49c740a --- /dev/null +++ b/wicket/src/test/java/de/thpeetz/kontor/Start.java @@ -0,0 +1,106 @@ +package de.thpeetz.kontor; + +import java.lang.management.ManagementFactory; + +import javax.management.MBeanServer; + +import org.eclipse.jetty.jmx.MBeanContainer; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.webapp.WebAppContext; + +/** + * Separate startup class for people that want to run the examples directly. Use parameter + * -Dcom.sun.management.jmxremote to startup JMX (and e.g. connect with jconsole). + */ +public class Start +{ + /** + * Main function, starts the jetty server. + * + * @param args + */ + public static void main(String[] args) + { + System.setProperty("wicket.configuration", "development"); + + Server server = new Server(); + + HttpConfiguration http_config = new HttpConfiguration(); + http_config.setSecureScheme("https"); + http_config.setSecurePort(8443); + http_config.setOutputBufferSize(32768); + + ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(http_config)); + http.setPort(8080); + http.setIdleTimeout(1000 * 60 * 60); + + server.addConnector(http); + + Resource keystore = Resource.newClassPathResource("/keystore.p12"); + if (keystore != null && keystore.exists()) + { + // if a keystore for a SSL certificate is available, start a SSL + // connector on port 8443. + // By default, the quickstart comes with a Apache Wicket Quickstart + // Certificate that expires about half way september 2031. Do not + // use this certificate anywhere important as the passwords are + // available in the source. + + SslContextFactory sslContextFactory = new SslContextFactory(); + sslContextFactory.setKeyStoreResource(keystore); + sslContextFactory.setKeyStorePassword("wicket"); + sslContextFactory.setKeyManagerPassword("wicket"); + + HttpConfiguration https_config = new HttpConfiguration(http_config); + https_config.addCustomizer(new SecureRequestCustomizer()); + + ServerConnector https = new ServerConnector(server, new SslConnectionFactory( + sslContextFactory, "http/1.1"), new HttpConnectionFactory(https_config)); + https.setPort(8443); + https.setIdleTimeout(500000); + + server.addConnector(https); + System.out.println("SSL access to the examples has been enabled on port 8443"); + System.out + .println("You can access the application using SSL on https://localhost:8443"); + System.out.println(); + } + + WebAppContext bb = new WebAppContext(); + bb.setServer(server); + bb.setContextPath("/"); + bb.setWar("src/main/webapp"); + + // uncomment the next two lines if you want to start Jetty with WebSocket (JSR-356) support + // you need org.apache.wicket:wicket-native-websocket-javax in the classpath! + // ServerContainer serverContainer = WebSocketServerContainerInitializer.configureContext(bb); + // serverContainer.addEndpoint(new WicketServerEndpointConfig()); + + // uncomment next line if you want to test with JSESSIONID encoded in the urls + // ((AbstractSessionManager) + // bb.getSessionHandler().getSessionManager()).setUsingCookies(false); + + server.setHandler(bb); + + MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); + MBeanContainer mBeanContainer = new MBeanContainer(mBeanServer); + server.addEventListener(mBeanContainer); + server.addBean(mBeanContainer); + + try + { + server.start(); + server.join(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(100); + } + } +} diff --git a/wicket/src/test/java/de/thpeetz/kontor/TestHomePage.java b/wicket/src/test/java/de/thpeetz/kontor/TestHomePage.java new file mode 100644 index 0000000..fe9c409 --- /dev/null +++ b/wicket/src/test/java/de/thpeetz/kontor/TestHomePage.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor; + +import org.apache.wicket.util.tester.WicketTester; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Simple test using the WicketTester + */ +public class TestHomePage +{ + private WicketTester tester; + + @BeforeEach + public void setUp() + { + tester = new WicketTester(new KontorApplication()); + } + + @Test + public void homepageRendersSuccessfully() + { + //start and render the test page + tester.startPage(HomePage.class); + + //assert rendered page class + tester.assertRenderedPage(HomePage.class); + } +} diff --git a/wicket/src/test/resources/jetty/jetty-http.xml b/wicket/src/test/resources/jetty/jetty-http.xml new file mode 100644 index 0000000..7b39acb --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty-http.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wicket/src/test/resources/jetty/jetty-https.xml b/wicket/src/test/resources/jetty/jetty-https.xml new file mode 100644 index 0000000..35100e7 --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty-https.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + http/1.1 + + + + + + + + + + + + + 30000 + + + + diff --git a/wicket/src/test/resources/jetty/jetty-ssl.xml b/wicket/src/test/resources/jetty/jetty-ssl.xml new file mode 100644 index 0000000..f23231b --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty-ssl.xml @@ -0,0 +1,57 @@ + + + + + + + + + / + + + + + + SSL_RSA_WITH_DES_CBC_SHA + SSL_DHE_RSA_WITH_DES_CBC_SHA + SSL_DHE_DSS_WITH_DES_CBC_SHA + SSL_RSA_EXPORT_WITH_RC4_40_MD5 + SSL_RSA_EXPORT_WITH_DES40_CBC_SHA + SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA + SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA + TLS_RSA_WITH_AES_256_GCM_SHA384 + TLS_RSA_WITH_AES_128_GCM_SHA256 + TLS_RSA_WITH_AES_256_CBC_SHA256 + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA + TLS_RSA_WITH_AES_256_CBC_SHA + TLS_RSA_WITH_AES_256_CBC_SHA + TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA + TLS_ECDH_RSA_WITH_AES_256_CBC_SHA + TLS_DHE_RSA_WITH_AES_256_CBC_SHA + TLS_DHE_DSS_WITH_AES_256_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA256 + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA + TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA + TLS_ECDH_RSA_WITH_AES_128_CBC_SHA + TLS_DHE_RSA_WITH_AES_128_CBC_SHA + TLS_DHE_DSS_WITH_AES_128_CBC_SHA + + + + + + + + + + + + + + + diff --git a/wicket/src/test/resources/jetty/jetty.xml b/wicket/src/test/resources/jetty/jetty.xml new file mode 100644 index 0000000..5590715 --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty.xml @@ -0,0 +1,23 @@ + + + + + + + + https + + + + 32768 + 8192 + 8192 + true + false + 512 + + + + + + diff --git a/wicket/src/test/resources/keystore b/wicket/src/test/resources/keystore new file mode 100644 index 0000000..1473db3 Binary files /dev/null and b/wicket/src/test/resources/keystore differ