/ app.py
app.py
1 from elements.core import Bottle, route, run, chevron_template, TEMPLATE_PATH, static_file, request, redirect 2 from elements.storage import save 3 from elements.posts import load_posts, SolarPost 4 from elements.accounts.sessions import load_session 5 from elements.api.accounts import login, logout 6 from collections import defaultdict 7 from datetime import datetime, timedelta 8 import csv 9 import os 10 11 ################################################## 12 ### Planet Data ################################## 13 ################################################## 14 15 app = Bottle() 16 path = '/credenso/' # How to get to this module from the root 17 name = "Credenso" 18 description = "Café and Community" 19 thumbnail = path + 'static/images/cred.png' 20 21 # If a planet is not defined as a station, it will still be available but 22 # its tag will not be shown on the main hub. 23 station = True 24 25 # This value refers to the folder that this file is stored in. 26 here = os.path.dirname(os.path.realpath(__file__)) 27 28 # By appending our views folder to the template path, 29 # We can render them with the template() functions. 30 TEMPLATE_PATH.append(f'{here}/views') 31 32 # Defaults are the values which are passed to every template 33 # in the current scope 34 def defaults(additional_data=None): 35 default_data = { 36 'path': path, 37 'static': path + 'static/' 38 } 39 40 # if there's extra data, (over)write it to the default info. 41 if additional_data is not None: 42 for key, value in additional_data.items(): 43 default_data[key] = value 44 45 return { 46 # This is the path from the Solar root directory 47 'partials_path': f'{ here }/views{path}components/', 48 49 # This is the filetype we use for components 50 'partials_ext': 'mo', 51 52 # This is the data that gets rendered into the template 53 'data': default_data 54 } 55 56 def slugify(string): 57 # filesafe name 58 s = string.lower().strip() 59 s = re.sub(r'[^\w\s-]', '', s) 60 s = re.sub(r'[\s_-]+', '-', s) 61 s = re.sub(r'^-+|-+$', '', s) 62 63 return s 64 65 ################################################## 66 ### API Section ################################## 67 ################################################## 68 69 # we register certain paths to preconfigured endpoints 70 # for use within the application 71 @app.post('/menu') 72 def menu_post(): 73 session = load_session() 74 try: 75 member = session.get('member') 76 except AttributeError: 77 # If getting the member fails, redirect to login 78 return redirect('/login') 79 80 post = SolarPost(request.forms, request.files, author=member, thumbnail=(200, 150)) 81 82 # Storing all posts in one directory may cause collisions, 83 # but that can be resolved by making them user-specific and 84 # storing in subdirectories 85 post.store(f'credenso/menu') 86 87 return redirect(request.environ.get('HTTP_REFERER')) 88 89 @app.post('/<month>/order') 90 def order_post(month): 91 honey = request.forms.get('honey') 92 if honey != '': 93 return redirect(request.environ.get('HTTP_REFERER')) 94 95 data = { 96 'name': request.forms.get('name'), 97 'contact': request.forms.get('contact'), 98 'info': request.forms.get('info') 99 } 100 101 writable_month = slugify(month) 102 103 save(data, 'subscriptions/' + writable_month, name=data.get('name')) 104 # Implement this soonish 105 #msg = Message(request.forms, to="zen", from="credenso") 106 #msg.send() 107 108 return redirect(f'{path}thanks.html') 109 110 @app.post('/finances') 111 def finances_post(): 112 session = load_session() 113 try: 114 member = session.get('member') 115 except AttributeError: 116 # If getting the member fails, redirect to login 117 return redirect('/login') 118 119 120 date = request.forms.get("date") 121 description = request.forms.get("description") 122 amount = int(request.forms.get("amount")) 123 amount = "{:0.2f}".format(abs(amount)) 124 125 entry = f'{date},{description},{amount}' 126 127 link = request.forms.get("link") 128 129 if link is not None: 130 entry += ',' + link 131 132 with open(f'{here}/credenso_budget.csv', 'a', newline='') as csvfile: 133 csvfile.write(entry) 134 135 # Storing all posts in one directory may cause collisions, 136 # but that can be resolved by making them user-specific and 137 # storing in subdirectories 138 139 return redirect(request.environ.get('HTTP_REFERER')) 140 141 app.post('/login', callback=login) 142 143 # When deployed, this path is managed by NGINX 144 @app.route('/static/<filepath:path>') 145 def static(filepath): 146 return static_file(filepath, root=f'{here}/static') 147 148 ################################################## 149 ### Frontend Routes ############################## 150 ################################################## 151 152 # These are the routes that expect to render content for the client 153 154 @app.route('/finances.html') 155 def finances(): 156 budget_info = defaultdict(list) 157 totals = defaultdict(int) 158 ledger = [] 159 160 with open(f'{here}/credenso_budget.csv', newline='') as csvfile: 161 budget = csv.DictReader(csvfile) 162 for row in budget: 163 # parse the date 164 date = datetime.strptime(row['Date'], '%Y-%m-%d') 165 year = date.strftime('%Y') 166 day = date.strftime('%b %d, %Y') 167 description = row['Description'] 168 link = row['Link'] 169 170 amount = float(row['Amount']) 171 172 totals[year] += amount 173 174 negative = amount < 0 175 176 budget_info[year].append({ 177 'date': date, 178 'day': day, 179 'description': description, 180 'amount': "{:0.2f}".format(abs(amount)), 181 'negative': negative, 182 'link': link 183 }) 184 185 for year in budget_info: 186 ledger.append({'year': year, 'entries': reversed(budget_info[year]), 'total': "{:0.2f}".format(abs(totals[year])), 'neg': totals[year] < 0 }) 187 188 data = { 189 'name': 'Credenso', 190 'label': 'Finances', 191 'ledger': reversed(ledger) 192 } 193 return chevron_template(f'credenso/finances.html', **defaults(data)); 194 195 @app.route('/admin.html') 196 def admin(): 197 session = load_session() 198 data = { 199 'session': session, 200 'name': 'Credenso', 201 'label': 'Admin' 202 } 203 return chevron_template(f'credenso/admin.html', **defaults(data)); 204 205 @app.route('/cafe.html') 206 def cafe(): 207 posts = load_posts('credenso/menu') 208 food = [] 209 drink = [] 210 etc = [] 211 for post in posts: 212 category = post.get('metadata').get('category') 213 if category == "food": 214 food.append(post) 215 elif category == "drink": 216 drink.append(post) 217 elif category == "other": 218 etc.append(post) 219 220 data = { 221 'name': 'Credenso', 222 'label': 'Café', 223 'menu': posts, 224 'food_menu': food, 225 'drink_menu': drink, 226 'etc_menu': etc 227 } 228 return chevron_template(f'credenso/cafe.html', **defaults(data)) 229 230 @app.route('/orders.html') 231 def orders(): 232 now = datetime.today() 233 next_month = (now.replace(day=1) + timedelta(days=32)) 234 data = { 235 'name': 'Credenso', 236 'next_month': next_month.strftime('%B %Y') 237 } 238 return chevron_template(f'credenso/orders.html', **defaults(data)) 239 240 @app.route('/<template>.html') 241 def index(template): 242 data = { 243 'name': 'Credenso' 244 } 245 return chevron_template(f'credenso/{template}.html', **defaults(data)) 246 247 @app.route('/') 248 def index(): 249 data = { 250 'name': 'Solar', 251 'main': True 252 } 253 return chevron_template('credenso/index.html', **defaults(data)) 254 255 256 ################################################## 257 ### Running the code ############################# 258 ################################################## 259 260 # Normally, a planet will be pushed into orbit by the main app within the Solar system. 261 # However, any app can serve as the main landing page if given access to elements 262 263 if __name__ == "__main__": 264 app.run(host='0.0.0.0', port=1618, debug=True)