diff --git a/chezbetty/__init__.py b/chezbetty/__init__.py index f977a5e..8817db9 100644 --- a/chezbetty/__init__.py +++ b/chezbetty/__init__.py @@ -101,12 +101,10 @@ def debug(request): config.add_route('shame', '/shame') config.add_route('shame_csv', '/shame.csv') - config.add_route('items', '/items') - config.add_route('item_request', '/item/request') - config.add_route('item_request_new', '/item/request/new') + config.add_route('items', '/items') - config.add_route('paydebt', '/paydebt/{uniqname}') - config.add_route('paydebt_submit', '/paydebt/{uniqname}/submit') + config.add_route('paydebt', '/paydebt/{uniqname}') + config.add_route('paydebt_submit', '/paydebt/{uniqname}/submit') # TERMINAL VIEWS @@ -139,6 +137,10 @@ def debug(request): config.add_route('user_deposit_cc_custom', '/user/deposit_cc/custom') config.add_route('user_deposit_cc_submit', '/user/deposit_cc/submit') + config.add_route('user_item_request', '/user/item/request') + config.add_route('user_item_request_new', '/user/item/request/new') + config.add_route('user_item_request_post_new', '/user/item/request/{id}/post/new') + config.add_route('user_pools', '/user/pools') config.add_route('user_pools_new_submit', '/user/pools/new/submit') config.add_route('user_pool', '/user/pool/{pool_id}') @@ -153,7 +155,8 @@ def debug(request): config.add_route('admin_index', '/admin') config.add_route('admin_index_dashboard', '/admin/dashboard') - config.add_route('admin_ajax_bool', '/admin/ajax/bool/{object}/{id}/{field}/{state}') + config.add_route('admin_ajax_bool', '/admin/ajax/bool/{object}/{id}/{field}/{value}') + config.add_route('admin_ajax_text', '/admin/ajax/text/{object}/{id}/{field}') config.add_route('admin_ajax_new', '/admin/ajax/new/{object}/{arg}') config.add_route('admin_ajax_connection', '/admin/ajax/connection/{object1}/{object2}/{arg1}/{arg2}') @@ -235,6 +238,7 @@ def debug(request): config.add_route('admin_password_edit_submit', '/admin/password/edit/submit') config.add_route('admin_requests', '/admin/requests') + config.add_route('admin_item_request_post_new', '/admin/item/request/{id}/post/new') config.add_route('admin_announcements_edit', '/admin/announcements/edit') config.add_route('admin_announcements_edit_submit', '/admin/announcements/edit/submit') diff --git a/chezbetty/datalayer.py b/chezbetty/datalayer.py index 7d694ca..9cc50a0 100644 --- a/chezbetty/datalayer.py +++ b/chezbetty/datalayer.py @@ -156,8 +156,8 @@ def delete_box(box): # Call this to make a new item request -def new_request(user, request_text): - r = request.Request(user, request_text) +def new_request(user, request_text, vendor, vendor_url=None): + r = request.Request(user, request_text, vendor, vendor_url) DBSession.add(r) DBSession.flush() return r diff --git a/chezbetty/jinja2_filters.py b/chezbetty/jinja2_filters.py index b9a4c6c..1bd2472 100644 --- a/chezbetty/jinja2_filters.py +++ b/chezbetty/jinja2_filters.py @@ -55,7 +55,9 @@ def shorten(s, l): return s[0:l-1] + '…' def make_link(obj, str_len=0): - if type(obj) is box.Box: + if obj is None: + return '' + elif type(obj) is box.Box: return '{}'.format(obj.id, shorten(obj.name, str_len)) elif type(obj) is item.Item: return '{}'.format(obj.id, shorten(obj.name, str_len)) diff --git a/chezbetty/migrations/migration_1.18.1-1.19.0.sql b/chezbetty/migrations/migration_1.18.1-1.19.0.sql new file mode 100644 index 0000000..78e75d8 --- /dev/null +++ b/chezbetty/migrations/migration_1.18.1-1.19.0.sql @@ -0,0 +1,28 @@ +/* WARN: This migration drops all old requests since the storage format changed significantly */ +COPY requests TO '/tmp/cb_old_requests.csv' DELIMITER ',' CSV HEADER; + +CREATE TABLE requests_pre_v1_19 AS SELECT * FROM requests; + +DELETE FROM requests; +ALTER TABLE requests ADD COLUMN vendor_id INTEGER NOT NULL; +ALTER TABLE requests ADD CONSTRAINT vendor_id FOREIGN KEY(vendor_id) REFERENCES vendors(id) MATCH FULL; +ALTER TABLE requests ADD COLUMN vendor_url TEXT; + +ALTER TABLE vendors ADD COLUMN product_urls BOOLEAN; +UPDATE vendors SET product_urls=True WHERE name='Amazon'; +UPDATE vendors SET product_urls=True WHERE name='TalDepot'; + +CREATE TABLE request_posts ( + id SERIAL, + "timestamp" timestamp without time zone NOT NULL, + request_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + post TEXT, + staff_post BOOLEAN NOT NULL DEFAULT FALSE, + deleted BOOLEAN NOT NULL DEFAULT FALSE +); +ALTER TABLE request_posts ADD CONSTRAINT request_id FOREIGN KEY(request_id) REFERENCES requests(id) MATCH FULL; +ALTER TABLE request_posts ADD CONSTRAINT user_id FOREIGN KEY(user_id) REFERENCES users(id) MATCH FULL; + +/* ALTER TABLE requests ADD COLUMN response TEXT; */ + diff --git a/chezbetty/models/request.py b/chezbetty/models/request.py index 3bf181b..fd66540 100644 --- a/chezbetty/models/request.py +++ b/chezbetty/models/request.py @@ -1,21 +1,47 @@ from .model import * +from . import vendor +from . import request_post from sqlalchemy_utils import ArrowType class Request(Base): __tablename__ = 'requests' - id = Column(Integer, primary_key=True, nullable=False) - timestamp = Column(ArrowType, nullable=False, default=datetime.datetime.utcnow) - user_id = Column(Integer, ForeignKey("users.id"), nullable=True) # user that made the request - request = Column(Text) - enabled = Column(Boolean, default=True, nullable=False) - deleted = Column(Boolean, default=False, nullable=False) + id = Column(Integer, primary_key=True, nullable=False) + timestamp = Column(ArrowType, nullable=False, default=datetime.datetime.utcnow) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + request = Column(Text) + vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False) + vendor_url = Column(Text) + enabled = Column(Boolean, default=True, nullable=False) + deleted = Column(Boolean, default=False, nullable=False) - def __init__(self, user, request): - if user: - self.user_id = user.id + vendor = relationship( + vendor.Vendor, + primaryjoin="and_(Request.vendor_id==Vendor.id, Request.deleted==False)", + backref="requests", + ) + + posts = relationship( + request_post.RequestPost, + primaryjoin="and_(RequestPost.request_id==Request.id, RequestPost.deleted==False)", + backref="request", + ) + deleted_posts = relationship( + request_post.RequestPost, + primaryjoin="and_(RequestPost.request_id==Request.id, RequestPost.deleted==True)", + ) + all_posts = relationship( + request_post.RequestPost, + primaryjoin="RequestPost.request_id==Request.id", + ) + + def __init__(self, user, request, vendor, vendor_url=None): + self.user_id = user.id self.request = request + self.vendor_id = vendor.id + if vendor_url: + self.vendor_url = vendor_url @classmethod def from_id(cls, id): @@ -23,7 +49,9 @@ def from_id(cls, id): @classmethod def all(cls): - return DBSession.query(cls).filter(cls.deleted==False).all() + return DBSession.query(cls).filter(cls.deleted==False)\ + .order_by(desc(cls.timestamp))\ + .all() @classmethod def count(cls): diff --git a/chezbetty/models/request_post.py b/chezbetty/models/request_post.py new file mode 100644 index 0000000..654f6dd --- /dev/null +++ b/chezbetty/models/request_post.py @@ -0,0 +1,33 @@ +from .model import * + +from sqlalchemy_utils import ArrowType + +class RequestPost(Base): + __tablename__ = 'request_posts' + + id = Column(Integer, primary_key=True, nullable=False) + timestamp = Column(ArrowType, nullable=False, default=datetime.datetime.utcnow) + request_id = Column(Integer, ForeignKey("requests.id"), nullable=False) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + post = Column(Text) + # Allow admin users to post as users or admins by tracking the view that the post is posted from + staff_post = Column(Boolean, default=False, nullable=False) + deleted = Column(Boolean, default=False, nullable=False) + + def __init__(self, request, user, post, staff_post=False, deleted=False): + self.request_id = request.id + self.user_id = user.id + self.post = post + self.staff_post = staff_post + self.deleted = deleted + + @classmethod + def from_id(cls, id): + return DBSession.query(cls).filter(cls.id == id).one() + + @classmethod + def all(cls): + return DBSession.query(cls).filter(cls.deleted==False)\ + .order_by(desc(cls.timestamp))\ + .all() + diff --git a/chezbetty/models/user.py b/chezbetty/models/user.py index cf7e965..d17bb64 100644 --- a/chezbetty/models/user.py +++ b/chezbetty/models/user.py @@ -5,6 +5,8 @@ from .model import * from . import account from . import event +from . import request +from . import request_post from chezbetty import utility import ldap3 @@ -120,6 +122,13 @@ class User(account.Account): administrative_events = relationship(event.Event, foreign_keys=[event.Event.user_id], backref="admin") events_deleted = relationship(event.Event, foreign_keys=[event.Event.deleted_user_id], backref="deleted_user") + requests = relationship(request.Request, foreign_keys=[request.Request.user_id], backref="user") + request_posts = relationship( + request_post.RequestPost, + foreign_keys=[request_post.RequestPost.user_id], + backref="user", + ) + __ldap = LDAPLookup() def __init__(self, uniqname, umid, name): diff --git a/chezbetty/models/vendor.py b/chezbetty/models/vendor.py index 36c1e5f..b410a65 100644 --- a/chezbetty/models/vendor.py +++ b/chezbetty/models/vendor.py @@ -3,10 +3,11 @@ class Vendor(Base): __tablename__ = 'vendors' - id = Column(Integer, primary_key=True, nullable=False) - name = Column(String(255), nullable=False) + id = Column(Integer, primary_key=True, nullable=False) + name = Column(String(255), nullable=False) - enabled = Column(Boolean, default=True, nullable=False) + enabled = Column(Boolean, default=True, nullable=False) + product_urls = Column(Boolean) def __init__(self, name, enabled=True): self.name = name diff --git a/chezbetty/static/css/chezbetty-common.css b/chezbetty/static/css/chezbetty-common.css index dc24e1c..fc7543a 100644 --- a/chezbetty/static/css/chezbetty-common.css +++ b/chezbetty/static/css/chezbetty-common.css @@ -21,6 +21,22 @@ padding: 2px 5px 2px 5px; } +.deleted { + background-color: #f0f0f0; + text-decoration: line-through; + display: none; +} + +/* Highlight a form element that's required that was not filled out */ +.form-required-missing { + background-color: #ff8080; + padding: 2px 5px 2px 5px; +} +.form-required-message { + color: red; + display: none; +} + /* Support for auto-sizing text */ .fitin { overflow: hidden; diff --git a/chezbetty/static/js/chezbetty-admin-onload.js b/chezbetty/static/js/chezbetty-admin-onload.js index d9283cd..966cd03 100644 --- a/chezbetty/static/js/chezbetty-admin-onload.js +++ b/chezbetty/static/js/chezbetty-admin-onload.js @@ -2,8 +2,8 @@ // Make the Demo Mode checkbox in the sidebar a pretty on/off slider $(".admin-switch").bootstrapSwitch(); -function ajax_bool (js_obj, object, field, id, status) { - var url = "/admin/ajax/bool/"+object+"/"+id+"/"+field+"/"+status; +function ajax_bool (js_obj, object, field, id, state) { + var url = "/admin/ajax/bool/"+object+"/"+id+"/"+field+"/"+state; $.ajax({ url: url, context: js_obj, diff --git a/chezbetty/static/js/chezbetty-common-onload.js b/chezbetty/static/js/chezbetty-common-onload.js index 2f45ad3..c6dd153 100644 --- a/chezbetty/static/js/chezbetty-common-onload.js +++ b/chezbetty/static/js/chezbetty-common-onload.js @@ -1,5 +1,16 @@ // single use button +$(".button-showhide").on('click', function () { + cls = $(this).attr('data-class'); + $('.'+cls).toggle(); + var alt_text = $(this).attr('data-alt-text'); + if ( alt_text != undefined ) { + var text = $(this).text(); + $(this).text(alt_text); + $(this).attr('data-alt-text', text); + } +}); + function button_singleuse_success (data) { if (data["status"] == "success") { $(this).hide(); @@ -25,8 +36,11 @@ $(".btn-ajax_singleuse").on('click', function () { function button_save_success (data) { if (data["status"] == "success") { - var input = $('#' + $(this).attr("id").slice(0,-4) + '-input'); - $(this).hide(); + var input = $(this); + var save = $('#' + $(this).attr("id") + '-btn-save' ); + var revert = $('#' + $(this).attr("id") + '-btn-revert'); + save.hide(); + revert.hide(); input.attr('data-initial', data["value"]); alert_success(data["msg"]); } else { @@ -35,32 +49,46 @@ function button_save_success (data) { } function button_save_fail (data) { - alert_error("Button save failed."); + alert_error("Error saving changes."); } -$(".btn-ajax_savefield").on('click', function () { - var url = $(this).attr("data-url"); - var id = $(this).attr("id").slice(0,-4); - var input = $('#' + $(this).attr("id").slice(0,-4) + '-input'); +function ajax_button_textlike (js_obj, object, field, id, value) { + var url = "/admin/ajax/text/"+object+"/"+id+"/"+field; $.ajax({ url: url, method: 'POST', data: { - 'pool' : id, - 'name' : input.val(), + 'value' : value, }, - context: $(this), + context: js_obj, success: button_save_success, error: button_save_fail }); +}; + +$(".ajax-textlike-btn-save").on('click', function () { + var fields = $(this).attr("id").split("-"); + var input = $('#' + $(this).attr("id").slice(0,-9)); + var value = input.val(); + ajax_button_textlike(input, fields[2], fields[3], fields[4], value); }); -$(".input-ajax_savefield").on('input', function () { - var btn = $('#' + $(this).attr("id").slice(0,-6) + '-btn'); +$(".ajax-textlike-btn-revert").on('click', function () { + var textarea_id = $(this).attr("id").slice(0,-11); + var textarea = $('#'+textarea_id); + textarea.val(textarea.attr('data-initial')); + textarea.trigger('input'); +}); + +$(".ajax-textlike").on('input', function () { + var save_btn = $('#' + $(this).attr("id") + '-btn-save'); + var revert_btn = $('#' + $(this).attr("id") + '-btn-revert'); if ($(this).attr('data-initial') != $(this).val()) { - btn.show(); + save_btn.show(); + revert_btn.show(); } else { - btn.hide(); + save_btn.hide(); + revert_btn.hide(); } }); @@ -114,3 +142,58 @@ $(".fitin").each(function () { } }); + +// Generic JS to disable a controlled element if this input is empty +$('.disable-controlled-when-empty').on('change input', function disable_controlled_on_change() { + var controlled = $('#' + $(this).attr('data-controlled')); + var contents = $.trim($(this).val()); + if ( contents == '' ) { + controlled.prop('disabled', true); + } else { + controlled.prop('disabled', false); + } +}); + + +// Generic JS to verify that all form fields have been filled out +$('.form-with-requirements').submit(function check_submit(evt) { + evt.preventDefault(); + + var missing_requirements = []; + $('.form-required').each(function check_submit_each(index) { + $('.form-required-message').hide(); + $(this).removeClass('form-required-missing'); + if ( $(this).is("input") ) { + if ( $(this).val() == '' ) { + missing_requirements.push($(this)); + } + } else if ( $(this).is("select") ) { + if ( $(this).val() == null ) { + missing_requirements.push($(this)); + } + } + }); + + if (missing_requirements.length != 0) { + $('.form-required-message').show(); + $.each(missing_requirements, function report_missing_requirement(index) { + $(this).addClass('form-required-missing'); + }); + return false; + } + + $(this).unbind('submit').trigger('submit'); +}); + + +// Item request page hook to selectively validate URL field +$('#request-vendor').change(function() { + var selected = $(this).find(":selected"); + if ( selected.attr('data-product-urls') == 'True' ) { + $('#request-vendor-url').addClass('form-required'); + } else { + $('#request-vendor-url').removeClass('form-required'); + $('#request-vendor-url').removeClass('form-required-missing'); + } +}); + diff --git a/chezbetty/templates/admin/base.jinja2 b/chezbetty/templates/admin/base.jinja2 index c64e3e1..171f6fc 100644 --- a/chezbetty/templates/admin/base.jinja2 +++ b/chezbetty/templates/admin/base.jinja2 @@ -14,6 +14,7 @@ + diff --git a/chezbetty/templates/admin/macro_buttons.jinja2 b/chezbetty/templates/admin/macro_buttons.jinja2 index f1266ab..b8718de 100644 --- a/chezbetty/templates/admin/macro_buttons.jinja2 +++ b/chezbetty/templates/admin/macro_buttons.jinja2 @@ -6,25 +6,6 @@ {% endmacro %} -{% macro onoff_switch(object, field, id, status) %} - -{% endmacro %} - -{% macro delete(object, field, id) %} - -{% endmacro %} - -{# Button that can only be clicked once. Will create an AJAX request to the - URL specified. Javascript will handle the response. If success, the button - will be removed and banner displayed. If failure, button will stay and error - will be displayed. - #} -{% macro ajax_singleuse_button(button_text, ajax_url, id) %} - -{% endmacro %} - -{% macro ajax_saveable_input(initial_value, ajax_url, id) %} - - -{% endmacro %} +{% set role = 'admin' %} +{% extends "../macro_buttons.jinja2" %} diff --git a/chezbetty/templates/admin/pool.jinja2 b/chezbetty/templates/admin/pool.jinja2 index 94e6592..928df5b 100644 --- a/chezbetty/templates/admin/pool.jinja2 +++ b/chezbetty/templates/admin/pool.jinja2 @@ -18,7 +18,7 @@
Name
-
{{ button.ajax_saveable_input(pool.name, "/admin/pool/"~pool.id~"/name", pool.id)}}
+
{{ button.ajax_saveable_input(pool, "pool", "name")}}
Owner
{{ pool_owner|make_link|safe }}
Balance
{{ pool.balance|format_currency|safe }}
Credit Limit
{{ pool.credit_limit|format_currency|safe }}
diff --git a/chezbetty/templates/admin/requests.jinja2 b/chezbetty/templates/admin/requests.jinja2 index 3756637..601c834 100644 --- a/chezbetty/templates/admin/requests.jinja2 +++ b/chezbetty/templates/admin/requests.jinja2 @@ -13,19 +13,75 @@ - Date - Request Delete + Date / User + Request / Response - {% for request in requests|reverse %} + {% for request in requests %} - {{ request.timestamp|pretty_date|safe }} - {{ request.request }} {{ button.delete("request", "deleted", request.id) }} + + {{ request.timestamp|pretty_date|safe }}
{{ request.user|make_link|safe }} + {% if request.deleted_posts %} +
+ {{ button.showhide("request-" + request.id|string + "-deleted", "Show deleted comments", "Hide deleted comments") }} + {% endif %} + + +
+
+
+
Item
+
{{ request.request }}
+
Store
+
{{ request.vendor.name }}
+ {% if request.vendor_url %} +
URL
+
{{ request.vendor_url }}
+ {% endif %} +
+
+
+ {% for post in request.all_posts %} +
+
+
+
+ {{ post.timestamp|human_date|safe}} +
+ {% if post.staff_post %} + Staff Post ({{ post.user|make_link|safe }}) + {% else %} + {{ post.user|make_link|safe }} + {% endif %} +
+
{{ post.post }}
+
+
+
+ {% if not post.deleted %} + {{ button.delete("request_post", "deleted", post.id) }} + {% endif %} +
+
+ {% endfor %} +
+
+
+
+ +
+
+ +
+
+
+
+ {% endfor %} diff --git a/chezbetty/templates/macro_buttons.jinja2 b/chezbetty/templates/macro_buttons.jinja2 index 52dfc86..45d7678 100644 --- a/chezbetty/templates/macro_buttons.jinja2 +++ b/chezbetty/templates/macro_buttons.jinja2 @@ -1,9 +1,17 @@ +{% if role is not defined %} +{% set role = 'user' %} +{% endif %} + +{% macro showhide(class, text="Show/Hide", alt_text=none) %} + +{% endmacro %} + {% macro onoff_switch(object, field, id, status) %} - + {% endmacro %} {% macro delete(object, field, id) %} - + {% endmacro %} {# Button that can only be clicked once. Will create an AJAX request to the @@ -14,3 +22,16 @@ {% macro ajax_singleuse_button(button_text, ajax_url, id) %} {% endmacro %} + +{% macro ajax_saveable_input(object, type, field) %} + + + +{% endmacro %} + +{% macro ajax_saveable_textarea(object, type, field) %} + + + +{% endmacro %} + diff --git a/chezbetty/templates/public/exception.jinja2 b/chezbetty/templates/public/exception.jinja2 index ab546fc..359d22d 100644 --- a/chezbetty/templates/public/exception.jinja2 +++ b/chezbetty/templates/public/exception.jinja2 @@ -14,6 +14,12 @@ well. Sorry for the trouble. Touch here to return to the home page. +{% if request.debug %} +Reload {{ request.GET['excepting_path'] }} +{% if excepting_path in request.GET %} +Reload {{ request.GET['excepting_path'] }} +{% endif %} +{% endif %} {% endblock %} diff --git a/chezbetty/templates/public/index.jinja2 b/chezbetty/templates/public/index.jinja2 index 9b32c40..4a41662 100644 --- a/chezbetty/templates/public/index.jinja2 +++ b/chezbetty/templates/public/index.jinja2 @@ -32,7 +32,11 @@ body {
Navigation
{{ _('Item List') }} - {{ _('Request an Item') }} + {% if request.user %} {# logged in? #} + {{ _('Request an Item') }} + {% else %} + {{ _('Request an Item') }} + {% endif %} {{ _('About') }} {{ _('Wall of Shame') }}
diff --git a/chezbetty/templates/public/item_request.jinja2 b/chezbetty/templates/public/item_request.jinja2 deleted file mode 100644 index 6a05064..0000000 --- a/chezbetty/templates/public/item_request.jinja2 +++ /dev/null @@ -1,43 +0,0 @@ -{% extends "base.jinja2" %} -{% block title %}{{ _('Item Request') }}{% endblock %} -{% block header %}{{ _('Request a New Item at Chez Betty') }}{% endblock %} - - - -{% block content %} - -

{{ _('Expanding the store.') }}

- -
-
-

{{ _('New Item Request') }}

-
-
- -

- {{ _('Enter an item you would like to see Betty sell. Please include ' - 'where you purchased the item from before so we have a chance at finding it. ' - 'We normally shop at Costco, Meijer, and Kroger, so if those stores ' - 'do not carry it, it is unlikely that Betty will either. ' - 'If you do not include where you know we can buy the item we will just delete ' - 'the request.') }} -

-

- {{ _('Also, depending on your motivation, you could be the supplier of a new item. ' - 'For instance, if you are grocery shopping you can pick up a few extra of ' - 'what you want, add it the Betty system, and see if it sells well. ' - 'The process is pretty simple (particularly for small numbers of items) ' - 'and we can reimburse you by just writing a check. Get in touch with ' - 'us if you are interested.') }} -

- - -
- -
- -
-
-
- -{% endblock %} diff --git a/chezbetty/templates/user/base.jinja2 b/chezbetty/templates/user/base.jinja2 index 1ad8e0a..8999505 100644 --- a/chezbetty/templates/user/base.jinja2 +++ b/chezbetty/templates/user/base.jinja2 @@ -14,6 +14,7 @@ + diff --git a/chezbetty/templates/user/item_request.jinja2 b/chezbetty/templates/user/item_request.jinja2 new file mode 100644 index 0000000..55f8de2 --- /dev/null +++ b/chezbetty/templates/user/item_request.jinja2 @@ -0,0 +1,131 @@ +{% extends "base.jinja2" %} +{% set active_page = 'item_request' %} +{% block title %}{{ _('Item Request') }}{% endblock %} +{% block header %}{{ _('Request a New Item at Chez Betty') }}{% endblock %} + + + +{% block content %} + +

{{ _('Expanding the store.') }}

+ +
+
+

{{ _('New Item Request') }}

+
+
+ +

+ {{ _('Depending on your motivation, you could be the supplier of a new item. ' + 'For instance, if you are grocery shopping you can pick up a few extra of ' + 'what you want, add it the Betty system, and see if it sells well. ' + 'The process is pretty simple (particularly for small numbers of items) ' + 'and we can reimburse you by just writing a check.') }} + {{ _('Get in touch with us if you are interested.') }} +

+ + +
+
+
+
{{ _('Item') }}
+
+
{{ _('Store') }}
+
+ +
+
+
{{ _('Betty only shops at a few regular stores. If these stores do not sell your item, Betty will not be able to carry it regularly, sorry.') }}
+
{{ _('Product URL') }}
+
+
+ +

* Please fill out all required fields

+
+
+
+ +
+
+

{{ _('Open Item Requests') }}

+
+
+ + + + + + + + + + {% for req in requests %} + + + + + {% endfor %} + +
WhenRequest
{{ req.timestamp|human_date|safe }} +
+
+
+
Item
+
{{ req.request }}
+
Store
+
{{ req.vendor.name }}
+ {% if req.vendor_url %} +
URL
+
{{ req.vendor_url }}
+ {% endif %} +
+
+
+ {% if req.posts %} +
+ {% endif %} + {% for post in req.posts %} +
+
+
+
+ {{ post.timestamp|human_date|safe}} +
+ {% if post.staff_post %} + Betty Staff + {% else %} + {{ post.user.name }} + {% endif %} +
+
{{ post.post }}
+
+
+
+ {% if request.user == post.user %} + {{ button.delete("request_post", "deleted", post.id) }} + {% endif %} +
+
+ {% endfor %} +
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+ +{% endblock %} diff --git a/chezbetty/templates/user/sidebar.jinja2 b/chezbetty/templates/user/sidebar.jinja2 index 4a3355f..5370e71 100644 --- a/chezbetty/templates/user/sidebar.jinja2 +++ b/chezbetty/templates/user/sidebar.jinja2 @@ -1,6 +1,7 @@