noranisa commited on
Commit
a815506
·
verified ·
1 Parent(s): a71af7c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +26 -92
app.py CHANGED
@@ -9,7 +9,7 @@ from flask import Flask, render_template, request, Response, url_for
9
  from flask_sqlalchemy import SQLAlchemy
10
  from flask_admin import Admin, AdminIndexView, expose
11
  from flask_admin.contrib.sqla import ModelView
12
- from flask_admin.form.upload import ImageUploadField
13
  from sqlalchemy import func
14
 
15
  # --- KONFIGURASI APLIKASI ---
@@ -17,36 +17,28 @@ app = Flask(__name__)
17
  app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-super-secret-key-for-local-dev')
18
 
19
  # --- KONFIGURASI DATABASE ---
20
- # Gunakan direktori /tmp yang dijamin bisa ditulisi di semua lingkungan container.
21
  DATA_DIR = '/tmp'
22
- # Pastikan direktori ada, buat jika belum ada.
23
  os.makedirs(DATA_DIR, exist_ok=True)
24
-
25
- # Tentukan path lengkap ke file database di dalam direktori /tmp
26
  db_path = os.path.join(DATA_DIR, 'database.db')
27
  app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
28
  app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
29
  db = SQLAlchemy(app)
30
 
31
-
32
  # --- MODEL DATABASE (STRUKTUR TABEL) ---
 
33
  class Product(db.Model):
34
  id = db.Column(db.Integer, primary_key=True)
35
  name = db.Column(db.String(100), unique=True, nullable=False)
36
  description = db.Column(db.Text, nullable=False)
37
  price = db.Column(db.String(50), nullable=False)
38
  image = db.Column(db.String(100), nullable=True, default='default.jpg')
39
-
40
- def __repr__(self):
41
- return f'<Product {self.name}>'
42
 
43
  class Setting(db.Model):
44
  id = db.Column(db.Integer, primary_key=True)
45
  key = db.Column(db.String(50), unique=True, nullable=False)
46
  value = db.Column(db.Text, nullable=True)
47
-
48
- def __repr__(self):
49
- return f'<Setting {self.key}>'
50
 
51
  class Sale(db.Model):
52
  id = db.Column(db.Integer, primary_key=True)
@@ -54,137 +46,82 @@ class Sale(db.Model):
54
  quantity = db.Column(db.Integer, nullable=False)
55
  total_price = db.Column(db.Float, nullable=False)
56
  sale_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
57
-
58
- def __repr__(self):
59
- return f'<Sale of {self.product_name}>'
60
-
61
 
62
  # --- INISIALISASI DATABASE OTOMATIS ---
63
  with app.app_context():
64
  db.create_all()
65
 
66
-
67
- # --- CONTEXT PROCESSOR UNTUK ADMIN TEMPLATE ---
68
  @app.context_processor
69
  def inject_admin_data():
70
- """Menyediakan data global ke semua template admin."""
71
  menu_icons = {
72
- 'Dashboard': 'fa-solid fa-chart-pie',
73
- 'Product': 'fa-solid fa-mug-hot',
74
- 'Sale': 'fa-solid fa-cash-register',
75
- 'Setting': 'fa-solid fa-sliders'
76
  }
77
-
78
  def get_setting_from_db(key, default=''):
79
  setting = Setting.query.filter_by(key=key).first()
80
  return setting.value if setting else default
81
-
82
  return dict(menu_icons=menu_icons, get_setting=get_setting_from_db)
83
 
84
-
85
- # --- FUNGSI KEAMANAN UNTUK ADMIN PANEL ---
86
  def check_auth(username, password):
87
  ADMIN_USER = os.environ.get('ADMIN_USER', 'admin')
88
  ADMIN_PASS = os.environ.get('ADMIN_PASS', 'password')
89
  return username == ADMIN_USER and password == ADMIN_PASS
90
 
91
  def authenticate():
92
- return Response(
93
- 'Could not verify your access level for that URL.\n'
94
- 'You have to login with proper credentials', 401,
95
- {'WWW-Authenticate': 'Basic realm="Login Required"'})
96
-
97
-
98
- # --- PENGATURAN ADMIN PANEL (DENGAN IMAGE UPLOAD) ---
99
-
100
- # Tentukan path untuk menyimpan gambar yang di-upload
101
- basedir = os.path.abspath(os.path.dirname(__file__))
102
- upload_path = os.path.join(basedir, 'static/img/')
103
- # Buat folder jika belum ada
104
- os.makedirs(upload_path, exist_ok=True)
105
-
106
 
 
107
  class AuthMixin:
108
- """Mixin untuk menambahkan autentikasi ke view admin."""
109
  def is_accessible(self):
110
  auth = request.authorization
111
  return auth and check_auth(auth.username, auth.password)
112
-
113
  def inaccessible_callback(self, name, **kwargs):
114
  return authenticate()
115
 
116
- # Buat view khusus untuk Produk agar bisa handle upload
117
- class ProductAdminView(AuthMixin, ModelView):
118
- # Gunakan form_extra_fields untuk menambahkan field upload
119
- form_extra_fields = {
120
- 'image': ImageUploadField(
121
- 'Image',
122
- base_path=upload_path,
123
- url_relative_path='img/',
124
- thumbnail_size=(100, 100, True) # Menampilkan thumbnail
125
- )
126
- }
127
-
128
  class SecureModelView(AuthMixin, ModelView):
129
  pass
130
 
131
  class DashboardView(AuthMixin, AdminIndexView):
 
132
  def get_sales_data(self):
133
- """Query data penjualan untuk 7 hari terakhir."""
134
- today = date.today()
135
- seven_days_ago = today - timedelta(days=6)
136
- sales = db.session.query(
137
- func.date(Sale.sale_date).label('date'),
138
- func.sum(Sale.total_price).label('total')
139
- ).filter(Sale.sale_date >= seven_days_ago).group_by(func.date(Sale.sale_date)).order_by(func.date(Sale.sale_date)).all()
140
  sales_dict = {s.date.strftime('%Y-%m-%d'): s.total for s in sales}
141
  labels = [(today - timedelta(days=i)).strftime('%a, %d') for i in range(6, -1, -1)]
142
  data = [sales_dict.get((today - timedelta(days=i)).strftime('%Y-%m-%d'), 0) for i in range(6, -1, -1)]
143
  return labels, data
144
-
145
  def get_stats(self):
146
- """Menghitung statistik utama untuk dashboard."""
147
- stats = {}
148
- total_revenue = db.session.query(func.sum(Sale.total_price)).scalar()
149
  stats['total_revenue'] = total_revenue or 0
150
  stats['total_products'] = Product.query.count()
151
- today_sales = Sale.query.filter(func.date(Sale.sale_date) == date.today()).count()
152
- stats['today_sales'] = today_sales
153
  return stats
154
-
155
  @expose('/')
156
  def index(self):
157
- chart_labels, chart_data = self.get_sales_data()
158
- stats = self.get_stats()
159
- return self.render('admin/dashboard.html',
160
- chart_labels=chart_labels,
161
- chart_data=chart_data,
162
- stats=stats)
163
-
164
- # Inisialisasi admin dengan struktur yang sudah diperbaiki
165
  admin = Admin(app, name='Bit & Bean Dashboard', template_mode='bootstrap4',
166
  base_template='admin/custom_master.html',
167
  index_view=DashboardView(name='Dashboard', url='/admin'))
168
 
169
- # Tambahkan view menggunakan kelas yang sesuai
170
- admin.add_view(ProductAdminView(Product, db.session)) # Gunakan view khusus Product
171
  admin.add_view(SecureModelView(Sale, db.session, category='Laporan'))
172
  admin.add_view(SecureModelView(Setting, db.session, category='Pengaturan'))
173
 
174
-
175
- # --- FUNGSI QR CODE ---
176
  def generate_qr_code(data):
177
  qr = qrcode.QRCode(version=1, box_size=10, border=4)
178
- qr.add_data(data)
179
- qr.make(fit=True)
180
  img = qr.make_image(fill_color="black", back_color="white")
181
- buffered = io.BytesIO()
182
- img.save(buffered, format="PNG")
183
  img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
184
  return img_str
185
 
186
-
187
- # --- ROUTE UNTUK HALAMAN UTAMA ---
188
  @app.route('/')
189
  def home():
190
  products_from_db = Product.query.all()
@@ -192,11 +129,8 @@ def home():
192
  for product in products_from_db:
193
  product_url = f"https://BitBean-company.hf.space/product/{product.id}"
194
  product_data = {
195
- "id": product.id,
196
- "name": product.name,
197
- "description": product.description,
198
- "price": product.price,
199
- "image": product.image,
200
  "qr_code": generate_qr_code(product_url)
201
  }
202
  products_with_qr.append(product_data)
 
9
  from flask_sqlalchemy import SQLAlchemy
10
  from flask_admin import Admin, AdminIndexView, expose
11
  from flask_admin.contrib.sqla import ModelView
12
+ # ImageUploadField tidak lagi digunakan
13
  from sqlalchemy import func
14
 
15
  # --- KONFIGURASI APLIKASI ---
 
17
  app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-super-secret-key-for-local-dev')
18
 
19
  # --- KONFIGURASI DATABASE ---
 
20
  DATA_DIR = '/tmp'
 
21
  os.makedirs(DATA_DIR, exist_ok=True)
 
 
22
  db_path = os.path.join(DATA_DIR, 'database.db')
23
  app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
24
  app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
25
  db = SQLAlchemy(app)
26
 
 
27
  # --- MODEL DATABASE (STRUKTUR TABEL) ---
28
+ # ... (Model tidak berubah) ...
29
  class Product(db.Model):
30
  id = db.Column(db.Integer, primary_key=True)
31
  name = db.Column(db.String(100), unique=True, nullable=False)
32
  description = db.Column(db.Text, nullable=False)
33
  price = db.Column(db.String(50), nullable=False)
34
  image = db.Column(db.String(100), nullable=True, default='default.jpg')
35
+ def __repr__(self): return f'<Product {self.name}>'
 
 
36
 
37
  class Setting(db.Model):
38
  id = db.Column(db.Integer, primary_key=True)
39
  key = db.Column(db.String(50), unique=True, nullable=False)
40
  value = db.Column(db.Text, nullable=True)
41
+ def __repr__(self): return f'<Setting {self.key}>'
 
 
42
 
43
  class Sale(db.Model):
44
  id = db.Column(db.Integer, primary_key=True)
 
46
  quantity = db.Column(db.Integer, nullable=False)
47
  total_price = db.Column(db.Float, nullable=False)
48
  sale_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
49
+ def __repr__(self): return f'<Sale of {self.product_name}>'
 
 
 
50
 
51
  # --- INISIALISASI DATABASE OTOMATIS ---
52
  with app.app_context():
53
  db.create_all()
54
 
55
+ # --- CONTEXT PROCESSOR ---
 
56
  @app.context_processor
57
  def inject_admin_data():
 
58
  menu_icons = {
59
+ 'Dashboard': 'fa-solid fa-chart-pie', 'Product': 'fa-solid fa-mug-hot',
60
+ 'Sale': 'fa-solid fa-cash-register', 'Setting': 'fa-solid fa-sliders'
 
 
61
  }
 
62
  def get_setting_from_db(key, default=''):
63
  setting = Setting.query.filter_by(key=key).first()
64
  return setting.value if setting else default
 
65
  return dict(menu_icons=menu_icons, get_setting=get_setting_from_db)
66
 
67
+ # --- FUNGSI KEAMANAN ---
 
68
  def check_auth(username, password):
69
  ADMIN_USER = os.environ.get('ADMIN_USER', 'admin')
70
  ADMIN_PASS = os.environ.get('ADMIN_PASS', 'password')
71
  return username == ADMIN_USER and password == ADMIN_PASS
72
 
73
  def authenticate():
74
+ return Response('Login Required', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
+ # --- PENGATURAN ADMIN PANEL (TANPA UPLOAD) ---
77
  class AuthMixin:
 
78
  def is_accessible(self):
79
  auth = request.authorization
80
  return auth and check_auth(auth.username, auth.password)
 
81
  def inaccessible_callback(self, name, **kwargs):
82
  return authenticate()
83
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  class SecureModelView(AuthMixin, ModelView):
85
  pass
86
 
87
  class DashboardView(AuthMixin, AdminIndexView):
88
+ # ... (Isi kelas DashboardView tidak berubah) ...
89
  def get_sales_data(self):
90
+ today = date.today(); seven_days_ago = today - timedelta(days=6)
91
+ sales = db.session.query(func.date(Sale.sale_date).label('date'), func.sum(Sale.total_price).label('total')).filter(Sale.sale_date >= seven_days_ago).group_by(func.date(Sale.sale_date)).order_by(func.date(Sale.sale_date)).all()
 
 
 
 
 
92
  sales_dict = {s.date.strftime('%Y-%m-%d'): s.total for s in sales}
93
  labels = [(today - timedelta(days=i)).strftime('%a, %d') for i in range(6, -1, -1)]
94
  data = [sales_dict.get((today - timedelta(days=i)).strftime('%Y-%m-%d'), 0) for i in range(6, -1, -1)]
95
  return labels, data
 
96
  def get_stats(self):
97
+ stats = {}; total_revenue = db.session.query(func.sum(Sale.total_price)).scalar()
 
 
98
  stats['total_revenue'] = total_revenue or 0
99
  stats['total_products'] = Product.query.count()
100
+ stats['today_sales'] = Sale.query.filter(func.date(Sale.sale_date) == date.today()).count()
 
101
  return stats
 
102
  @expose('/')
103
  def index(self):
104
+ chart_labels, chart_data = self.get_sales_data(); stats = self.get_stats()
105
+ return self.render('admin/dashboard.html', chart_labels=chart_labels, chart_data=chart_data, stats=stats)
106
+
 
 
 
 
 
107
  admin = Admin(app, name='Bit & Bean Dashboard', template_mode='bootstrap4',
108
  base_template='admin/custom_master.html',
109
  index_view=DashboardView(name='Dashboard', url='/admin'))
110
 
111
+ # Menggunakan SecureModelView standar untuk semua
112
+ admin.add_view(SecureModelView(Product, db.session))
113
  admin.add_view(SecureModelView(Sale, db.session, category='Laporan'))
114
  admin.add_view(SecureModelView(Setting, db.session, category='Pengaturan'))
115
 
116
+ # ... (Sisa kode, fungsi QR code dan route home, tidak berubah) ...
 
117
  def generate_qr_code(data):
118
  qr = qrcode.QRCode(version=1, box_size=10, border=4)
119
+ qr.add_data(data); qr.make(fit=True)
 
120
  img = qr.make_image(fill_color="black", back_color="white")
121
+ buffered = io.BytesIO(); img.save(buffered, format="PNG")
 
122
  img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
123
  return img_str
124
 
 
 
125
  @app.route('/')
126
  def home():
127
  products_from_db = Product.query.all()
 
129
  for product in products_from_db:
130
  product_url = f"https://BitBean-company.hf.space/product/{product.id}"
131
  product_data = {
132
+ "id": product.id, "name": product.name, "description": product.description,
133
+ "price": product.price, "image": product.image,
 
 
 
134
  "qr_code": generate_qr_code(product_url)
135
  }
136
  products_with_qr.append(product_data)